직접 할당 진단Diagnosing direct allocations

C++/WinRT를 통한 API 작성에서 설명한 대로 구현 형식의 개체를 만드는 경우 winrt::make 도우미 패밀리를 사용하여 이 작업을 수행해야 합니다.As explained in Author APIs with C++/WinRT, when you create an object of implementation type, you should use the winrt::make family of helpers to do so. 이 항목에서는 구현 형식의 개체를 스택에 직접 할당하는 실수를 진단하는 데 도움이 되는 C++/WinRT 2.0 기능에 대해 자세히 설명합니다.This topic goes in-depth on a C++/WinRT 2.0 feature that helps you to diagnose the mistake of directly allocating an object of implementation type on the stack.

이러한 실수는 디버깅하기가 어렵고 시간이 많이 걸리는 알 수 없는 크래시 또는 손상으로 변할 수 있습니다.Such mistakes can turn into mysterious crashes or corruptions that are difficult and time-consuming to debug. 따라서 이는 중요한 특징이며, 배경 지식을 이해할 가치가 있습니다.So this is an important feature, and it's worth understanding the background.

MyStringable을 사용하여 장면 설정Setting the scene, with MyStringable

먼저 IStringable의 간단한 구현을 살펴보겠습니다.First, let's consider a simple implementation of IStringable.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const { return L"MyStringable"; }
};

이제 IStringable을 인수로 예상하는 함수를 구현 내에서 호출해야 한다고 가정합니다.Now imagine that you need to call a function (from within your implementation) that expects an IStringable as an argument.

void Print(IStringable const& stringable)
{
    printf("%ls\n", stringable.ToString().c_str());
}

문제는 MyStringable 형식이 IStringable아니라는 것입니다.The trouble is that our MyStringable type is not an IStringable.

  • MyStringable 형식은 IStringable 인터페이스의 구현입니다.Our MyStringable type is an implementation of the IStringable interface.
  • IStringable 형식은 프로젝션된 형식입니다.The IStringable type is a projected type.

중요

구현 형식프로젝션된 형식의 구분을 이해해야 중요합니다.It's important to understand the distinction between an implementation type and a projected type. 필수 개념 및 용어에 대해서는 C++/WinRT를 통한 API 사용C++/WinRT를 통한 API 작성을 참조하세요.For essential concepts and terms, be sure to read Consume APIs with C++/WinRT and Author APIs with C++/WinRT.

구현과 프로젝션 사이의 공간을 이해하기가 미묘할 수 있습니다.The space between an implementation and the projection can be subtle to grasp. 그리고 실제로 구현이 프로젝션과 좀 더 비슷하게 느낄 수 있도록 하기 위해 구현은 구현하는 각 프로젝션된 형식에 대한 암시적 변환을 제공합니다.And in fact, to try to make the implementation feel a bit more like the projection, the implementation provides implicit conversions to each of the projected types that it implements. 그렇다고 해서 이렇게 간단히 수행할 수 있는 것은 아닙니다.That doesn't mean we can simply do this.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const;
 
    void Call()
    {
        Print(this);
    }
};

대신, 변환 연산자가 호출을 해결하기 위한 후보로 사용될 수 있도록 참조를 가져와야 합니다.Instead, we need to get a reference so that conversion operators may be used as candidates for resolving the call.

void Call()
{
    Print(*this);
}

이제 작동합니다.That works. 암시적 변환은 구현 형식에서 프로젝션된 형식으로의 매우 효율적인 변환을 제공하며, 많은 시나리오에서 매우 편리합니다.An implicit conversion provides a (very efficient) conversion from the implementation type to the projected type, and that's very convenient for many scenarios. 이 기능이 없으면 많은 구현 형식을 작성하는 것이 매우 번거로울 수 있습니다.Without that facility, a lot of implementation types would prove very cumbersome to author. winrt::make 함수 템플릿(또는 winrt::make_self)만 사용하여 구현을 할당하면 모든 것이 제대로 작동합니다.Provided that you only use the winrt::make function template (or winrt::make_self) to allocate the implementation, then all is well.

IStringable stringable{ winrt::make<MyStringable>() };

C++/WinRT 1.0의 잠재적 문제Potential pitfalls with C++/WinRT 1.0

암시적 변환은 여전히 곤란한 문제를 일으킬 수 있습니다.Still, implicit conversions can land you in trouble. 도움이 되지 않는 다음 도우미 함수를 살펴보겠습니다.Consider this unhelpful helper function.

IStringable MakeStringable()
{
    return MyStringable(); // Incorrect.
}

심지어 무해한 다음 명령문도 마찬가지입니다.Or even just this apparently harmless statement.

IStringable stringable{ MyStringable() }; // Also incorrect.

불행히도 이러한 암시적 변환으로 인해 C++/WinRT 1.0으로 컴파일된 코드가 있습니다.Unfortunately, code like that did compile with C++/WinRT 1.0, because of that implicit conversion. 매우 심각한 문제는 지원 메모리가 사용 후 삭제 스택에 있는 참조 수 계산 개체를 가리키는 프로젝션된 형식을 반환할 가능성이 있다는 것입니다.The (very serious) problem is that we're potentially returning a projected type that points to a reference-counted object whose backing memory is on the ephemeral stack.

C++/WinRT 1.0으로 컴파일된 다른 항목은 다음과 같습니다.Here's something else that compiled with C++/WinRT 1.0.

MyStringable* stringable{ new MyStringable() }; // Very inadvisable.

원시 포인터는 위험하고 노동 집약적인 버그의 원본입니다.Raw pointers are dangerous and labor-intensive source of bugs. 필요하지 않은 경우에는 사용하지 마세요.Don't use them if you don't need to. C++/WinRT는 원시 포인터를 사용하지 않고도 모든 작업을 효율적으로 수행하는 방식으로 진행됩니다.C++/WinRT goes out of its way to make everything efficient without ever forcing you into using raw pointers. C++/WinRT 1.0으로 컴파일된 다른 항목은 다음과 같습니다.Here's something else that compiled with C++/WinRT 1.0.

auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.

이는 여러 수준에서 나타나는 실수입니다.This is a mistake on several levels. 동일한 개체에 대해 서로 다른 두 개의 참조 수가 있습니다.We have two different reference counts for the same object. Windows 런타임(및 이전의 클래식 COM)은 std:shared_ptr과 호환되지 않는 내장 참조 수를 기반으로 합니다.The Windows Runtime (and classic COM before it) is based on an intrinsic reference count that's not compatible with std::shared_ptr. 물론 std::shared_ptr은 많은 유효한 애플리케이션을 포함하고 있지만, Windows 런타임(및 클래식 COM) 개체를 공유하는 경우에는 전혀 필요하지 않습니다.std::shared_ptr has, of course, many valid applications; but it's entirely unnecessary when you're sharing Windows Runtime (and classic COM) objects. 마지막으로 C++/WinRT 1.0으로도 컴파일됩니다.Finally, this also compiled with C++/WinRT 1.0.

auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.

여기에는 의심의 여지가 다시 있습니다.This is again rather questionable. 고유한 소유권이 MyStringable의 내장 참조 수에 대한 공유 수명과 반대입니다.The unique ownership is in opposition to the shared lifetime of the MyStringable's intrinsic reference count.

C++/WinRT 2.0을 사용하는 솔루션The solution with C++/WinRT 2.0

C++/WinRT 2.0을 사용하면 구현 형식을 직접 할당하려는 이러한 모든 시도로 인해 컴파일러 오류가 발생합니다.With C++/WinRT 2.0, all of these attempts to directly allocate implementation types leads to a compiler error. 이는 가장 적합한 오류이고, 알 수 없는 런타임 버그보다 훨씬 더 좋습니다.That's the best kind of error, and infinitely better than a mysterious runtime bug.

구현을 수행해야 할 때마다 위와 같이 winrt::make 또는 winrt::make_self를 사용하기만 하면 됩니다.Whenever you need to make an implementation, you can simply use winrt::make or winrt::make_self, as shown above. 그리고 이제 이 작업을 수행하는 것을 잊어버리면 use_make_function_to_create_this_object라는 추상 함수에 대한 참조를 사용하여 이를 암시하는 컴파일러 오류가 표시됩니다.And now, if you forget to do so, then you'll be greeted with a compiler error alluding to this with a reference to an abstract function named use_make_function_to_create_this_object. 정확히 static_assert는 아니지만 가깝습니다.It's not exactly a static_assert; but it's close. 그럼에도 불구하고, 이는 설명된 모든 실수를 검색하는 가장 신뢰할 수 있는 방법입니다.Still, this is the most reliable way of detecting all of the mistakes described.

구현에 몇 가지 사소한 제약 조건을 적용해야 한다는 것을 의미합니다.It does mean that we need to place a few minor constraints on the implementation. 직접 할당을 검색하기 위한 재정의가 없는 경우 winrt::make 함수 템플릿은 어떻게든 재정의를 사용하여 추상 가상 함수를 충족해야 합니다.Given that we're relying on the absence of an override to detect direct allocation, the winrt::make function template must somehow satisfy the abstract virtual function with an override. 이를 위해 재정의를 제공하는 final 클래스를 사용하여 구현에서 파생됩니다.It does so by deriving from the implementation with a final class that provides the override. 이 프로세스에 대해 관찰해야 할 몇 가지 사항이 있습니다.There are a few things to observe about this process.

첫째, 가상 함수는 디버그 빌드에만 있습니다.First, the virtual function is only present in debug builds. 즉 검색이 최적화된 빌드의 vtable 크기에는 영향을 주지 않습니다.Which means that detection isn't going to affect the size of the vtable in your optimized builds.

둘째, winrt::make에서 사용하는 파생 클래스가 final이므로, 이전에 구현 클래스를 final로 표시하지 않도록 선택한 경우에도 최적화 프로그램에서 추론할 수 있는 모든 가상화 해제가 발생한다는 것을 의미합니다.Second, since the derived class that winrt::make uses is final, it means that any devirtualization that the optimizer can possibly deduce will happen even if you previously chose not to mark your implementation class as final. 따라서 이는 향상된 기능입니다.So that's an improvement. 반대의 경우는 구현이 final될 수 없다는 것입니다.The converse is that your implementation can't be final. 다시 말하지만, 인스턴스화된 형식은 항상 final이므로 결과는 아무 의미가 없습니다.Again, that's of no consequence because the instantiated type will always be final.

셋째, 구현에서 가상 함수를 final로 표시할 수 없습니다.Third, nothing prevents you from marking any virtual functions in your implementation as final. 물론 C++/WinRT는 구현에 관한 모든 것이 가상화되는 경향이 있는 WRL과 같은 클래식 COM 및 구현과는 매우 다릅니다.Of course, C++/WinRT is very different from classic COM and implementations such as WRL, where everything about your implementation tends to be virtual. C++/WinRT에서 가상 디스패치는 ABI(애플리케이션 이진 인터페이스)(항상 final)로 제한되며, 구현 메서드는 컴파일 시간 또는 정적 다형성을 사용합니다.In C++/WinRT, the virtual dispatch is limited to the application binary interface (ABI) (which is always final), and your implementation methods rely on compile-time or static polymorphism. 이렇게 하면 불필요한 런타임 다형성을 방지하고, C++/WinRT 구현에서 가상 함수를 사용할 이유도 거의 없습니다.That avoids unnecessary runtime polymorphism, and also means that there's precious little reason for virtual functions in your C++/WinRT implementation. 이는 매우 좋은 방법이고, 훨씬 더 예측 가능한 인라인으로 이어집니다.Which is a very good thing, and leads to far more predictable inlining.

넷째, winrt::make는 파생 클래스를 삽입하므로 구현에는 프라이빗 소멸자가 있을 수 없습니다.Fourth, since winrt::make injects a derived class, your implementation can't have a private destructor. 모든 것이 가상이고 일반적으로 원시 포인터를 직접 처리했으며 이에 따라 실수로 Release 대신 delete를 호출하는 것이 쉬웠으므로 프라이빗 소멸자는 클래식 COM 구현에서 인기가 높았습니다.Private destructors were popular with classic COM implementations because, again, everything was virtual, and it was common to deal directly with raw pointers and thus was easy to accidentally call delete instead of Release. C++/WinRT에서는 원시 포인터를 직접 처리하기가 어렵습니다.C++/WinRT goes out of its way to make it hard for you to deal directly with raw pointers. 그리고 C++/WinRT에서 잠재적으로 delete를 호출할 수 있는 원시 포인터를 가져오도록 실제로 노력해야 합니다.And you'd have to really go out of your way to get a raw pointer in C++/WinRT that you could potentially call delete on. 값 의미 체계는 값과 참조를 처리한다는 것을 의미하며 포인터를 사용하는 경우는 거의 없습니다.Value semantics means that you're dealing with values and references; and rarely with pointers.

따라서 C++/WinRT는 클래식 COM 코드를 작성하는 것이 무엇을 의미하는지에 대한 선입견에 도전합니다.So, C++/WinRT challenges our preconceived notions of what it means to write classic COM code. 그리고 WinRT는 클래식 COM이 아니므로 완벽하게 합리적입니다.And that's perfectly reasonable because WinRT is not classic COM. 클래식 COM은 Windows 런타임의 어셈블리 언어이며,Classic COM is the assembly language of the Windows Runtime. 매일 작성하는 코드가 아니어야 합니다.It shouldn't be the code you write every day. 대신, C++/WinRT를 통해 최신 C++와 매우 비슷하고 클래식 COM보다 훨씬 덜 복잡한 코드를 작성할 수 있습니다.Instead, C++/WinRT gets you to write code that's more like modern C++, and far less like classic COM.

중요 APIImportant APIs