Microsoft MVP성태의 닷넷 이야기
VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [링크 복사], [링크+제목 복사],
조회: 7825
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 3개 있습니다.)

CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌

별다른 생성자를 추가하지 않은 경우, 클래스의 형변환은 상위 클래스로는 가능해도 하위 클래스로는 불가능합니다. 간단하게 테스트를 해볼까요?

class MyType
{
public:
    int f = 100;

    MyType() { }
};

class MyDervied : public MyType
{
public:
    bool result = true;

    MyDervied() { f = 200; }
};

위와 같은 클래스 구조에서, 다음과 같이 코드를 사용하면,

MyDervied test;
MyType t = test;

printf("test = %d, t = %d\n", t.f, test.f); // t = 200, test = 200

정상적으로 컴파일 및 빌드가 됩니다. 하지만, 반대로 부모 인스턴스를 자식 인스턴스에 대입하려는 시도는,

MyType t;
MyDervied test = t; // Error C2440 'initializing': cannot convert from 'MyType' to 'MyDervied'

보는 바와 같이 오류가 발생합니다.




위의 코드를 개선해 볼까요? 우선 자식을 부모 클래스에 대입하는 경우는 비록 컴파일은 가능하지만 Visual Studio의 코드 편집기 상에서 "t = test" 코드에 밑줄이 표시되면서 "C26437: Do not slice (es.63)" 경고를 알려줍니다. 이 경고를 없애려면, 명시적으로 부모 타입에 생성자를 하나 추가해 주면 됩니다.

class MyType
{
public:
    int f = 100;

    MyType() { }

    MyType(MyType& inst)
    {
        this->f = inst.f;
    }
};

그리고, 부모 인스턴스를 자식 인스턴스에 대입하는 것도 역시 자식 인스턴스에서 부모 인스턴스를 받아들이는 생성자를 정의해 주면 됩니다.

class MyDervied : public MyType
{
public:
    bool result = true;

    MyDervied() { this->f = 200; }

    MyDervied(MyType& inst)
    {
        this->f = inst.f;
    }
};

MyType t;
MyDervied test = t; // 정상적으로 컴파일

일단, 여기까지는 다들 아시는 그대로입니다.




이 상황에서 template을 사용하면 어떻게 될까요?

template <class T>
class MyType
{
public:
    int f = 100;

    MyType() { }

    MyType(MyType& inst)
    {
       this->f = inst.f;
    }
};

template <class T>
class MyDervied : public MyType<T>
{
public:
    bool result = true;

    MyDervied() { this->f = 200; }

    MyDervied(MyType<T>& inst)
    {
        this->f = inst.f;
    }
};

일단, 위와 같은 경우 다음과 같은 식으로는 잘 컴파일이 됩니다.

{
    MyDervied<int> test;
    MyType<int> t = test;

    printf("t = %d, test = %d\n", t.f, test.f);
}

{
    MyType<int> t;
    MyDervied<int> test = t;

    printf("t = %d, test = %d\n", t.f, test.f);
}

하지만, 만약 템플릿 타입 인자가 다르다면 어떻게 될까요?

{
    MyDervied<int> test;
    MyType<long> t = test; // Error C2440 'initializing': cannot convert from 'MyDervied' to 'MyType'
}

이런 경우, 컴파일러는 타입이 다르기 때문에 당연히 오류가 발생합니다.




그런데, 저 템플릿 타입이 달라도 컴파일이 되고 싶은 경우가 있습니다. 다소 특수한 사례를 들어서, 템플릿 인자 간의 상속 관계가 있는 경우 형변환을 허용하고 싶다면 다음과 같은 식으로 코딩을 할 수 있습니다.

class Root { };

class Node : public Root { };

class Terminal : public Root { };

template <class T>
class MyType
{
public:
    MyType() { }

    operator T& () { return p; }

    T p;
};

template <class T>
class MyDerived : public MyType<T>
{
public:
    MyDerived() { }

    MyDerived(Root& inst)
    {
    }
};

위의 코드는 이렇게 사용하는 것이 가능합니다. (자식 타입으로 형변환하고 있습니다.)

{
    MyType<Terminal> t;
    MyDerived<Node> test = t;
}

잘 보시면, t 인스턴스는 MyType의 T&() 연산자로 인해 Terminal 타입에 해당하는 인스턴스를 반환합니다. 그리고 MyDerived의 생성자는 Root& 타입을 인자로 정의했으므로 Terminal 타입의 인스턴스를 받을 수 있습니다.

여기서 재미있는 것은, 저 코드가 Visual C++의 "Conformance mode"를 적용하는 경우에는 컴파일 오류가 발생한다는 점입니다. 실제로 프로젝트 속성 창에서 "Configuration Properties" / "C/C++" / "Language" 범주로 들어가 "Conformance mode" 옵션을 "Yes (/permissive-)"로 켜면,

atl_cpp_conformance_mode_0.png

이후 "test = t;" 코드에서 다음과 같은 컴파일 오류가 발생합니다.

Error C2440 'initializing': cannot convert from 'MyType<Terminal>' to 'MyDerived<Node>'

즉, 무조건 템플릿 타입 인자가 같아야 한다는 것을 강제하는 것입니다. 하지만 템플릿 타입을 같게 만들어도 여전히 컴파일 오류가 발생합니다.

{
    MyType<Terminal> t;
    MyDerived<Terminal> test = t; // Error C2440 'initializing': cannot convert from 'MyType<T>' to 'MyDerived<Terminal>'
}

왜냐하면, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대입하려 했기 때문입니다. 따라서 다시 원론적으로 돌아와서, 상속받은 타입에서 부모 클래스의 인스턴스로 형변환하는 것만을 허용하게 된 것입니다.

{
    MyDerived<Terminal> test;
    MyType<Terminal> t = test;
}

즉, 이것은 템플릿을 사용하지 않던 원래의 클래스 타입과 동일한 규칙이 적용되는 것이어서 이제는 별다른 생성자도 필요하지 않습니다. 따라서, 위와 같은 코드는 그냥 다음과 같이 기본 클래스를 정의해도 가능한 수준입니다.

template <class T>
class MyType
{
public:
    MyType() { }
};

template <class T>
class MyDerived : public MyType<T>
{
public:
    MyDerived() { }
};




아니... 그나저나 도대체 저런 이상한 형태의 클래스 상속을 누가 사용한단 말입니까? ^^; 자, 서론이 길었군요. 바로 저런 식으로 사용하는 사례가 CComPtr과 CComQIPtr입니다.

이 타입들을 축약하면 다음과 같이 정의할 수 있습니다.

#include <d3d11.h>
#include <Unknwnbase.h>

template <class T>
class CComPtr
{
public:
    CComPtr() { p = nullptr; }

    CComPtr(T* lp) { p = lp; }

    operator T* () { return p; }

    T* p;
};

template <class T>
class CComQIPtr : public CComPtr<T>
{
public:
    CComQIPtr() { }

    CComQIPtr(IUnknown* lp) { }
};

보시면, CComQIPtr 생성자에서 IUnknown* 인자를 받아들이고, CComPtr에서는 T*() 연산자 오버로드로 포인터를 반환하고 있습니다. 즉, IUnknown을 상속한 타입을 템플릿 인자로 지정한 CComPtr 인스턴스는 CComQIPtr에 이렇게 대입하는 것이 가능합니다.

// IDXGIResource와 ID3D11Texture2D는 서로 다른 타입이지만, IUnknown을 상속
{
    CComPtr<IDXGIResource> pUnk;
    CComQIPtr<ID3D11Texture2D> pDisp = pUnk;
}

어차피 내부에서 IUnknown의 QueryInterface 함수를 호출하기 때문에 저렇게 사용하는 것은 어떠한 버그도 발생시키지 않습니다. 하지만, 저 코드 역시 마찬가지로 "Conformance mode"를 켜면 이렇게 오류가 발생합니다.

Error C2440 'initializing': cannot convert from 'CComPtr<IDXGIResource>' to 'CComQIPtr<ID3D11Texture2D>'




이 상황을 해결하려면 간단하게는 "Conformance" 모드를 끄면 됩니다. 하지만, 근래의 C++ 표준 준수를 위한 분위기를 고려한다면 좋은 선택이 아닙니다.

그렇다면, 다시 원론적으로 문제를 해결하는 수밖에는 없습니다. 일단, 대입은 안 되므로 다음과 같은 식으로 QueryInterface를 풀어쓰면서 스마트 포인터는 나름대로 그와 연동해 사용하는 것입니다.

// 이 한 줄의 코드를,
// CComQIPtr<ID3D11Texture2D> spTextureResource = spDXGIResource;
    
// 이렇게 풀어서 적용
CComQIPtr<ID3D11Texture2D> spTextureResource;
{
    ID3D11Texture2D* pTextureResource;
    hr = spDXGIResource->QueryInterface(__uuidof(ID3D11Texture2D), (LPVOID*)&pTextureResource);
    if (hr == S_OK)
    {
        spTextureResource.Attach(pTextureResource);
    }
}

좀 멋은 없어졌지만... 뭐 그래도... ^^;




그나저나, 갑자기 저 문제가 왜 발생한 것일까요? ^^ 문서를 보면 이에 대한 설명이 나옵니다.

/permissive- (Standards conformance)
; https://learn.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance

이 기능은 Visual Studio 2017부터 15.5 버전 이후로 기본값으로 바뀌었다고 합니다. 그러니까, "DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)" 글을 썼던 게 2017-12-05일이니까 아마도 그때 당시에는 /permissive+ 상태가 기본값이어서 아무런 문제가 없었던 것입니다.

하지만, 15.5 버전 패치가 적용된 이후부터는 새 프로젝트를 만들어 저 소스코드를 복사해 적용한 사용자들의 경우에는 /permissive- 옵션의 영향으로 컴파일 오류가 발생했던 것입니다.

참고로, /permissive- 옵션은 C++ 표준 옵션을 "Preview - Features from the Latest C++ Working Draft (/std:c++latest)" 또는 "/std:c++20"으로 설정하면 암시적으로 켜진다고 합니다.




그런데, 생각보다 커뮤니티의 분위기가 조용합니다. 관련해서 검색해 보면 아래의 이슈 하나가 나오는데요,

afxhtml.h fails to compile when disabling permissive mode
; https://developercommunity.visualstudio.com/t/afxhtmlh-fails-to-compile-when-disabling-permissiv/471981

재현 예제 코드를 정리하면 결국 이것과 같습니다.

#include <atlbase.h>

int main()
{
    CComPtr<IUnknown> pUnk;
    CComQIPtr<IDispatch> pDisp = pUnk;
}

// After importing a TLB, how do I convert from one type of _com_ptr_t to another?
// https://devblogs.microsoft.com/oldnewthing/20221228-00/?p=107621

이슈 대응을 보면 "Visual Studio 2019 버전 16.3"부터 해결되었다고 나옵니다. 이상하군요, 전혀 해결되지 않았는데... 혹시 제가 모르는 또 다른 옵션이 있을까요? ^^ 암튼, 근래에는 아무래도 ATL COM의 분위기가 많이 식었기 때문에 그에 따라 CComPtr과 CComQIPtr을 사용하는 경우도 많지 않은 듯합니다.




참고로, 예전에 Conformance 모드 관련해 써 놓은 글이 하나 있군요. ^^

C++의 연산자 동의어(operator synonyms), 대체 토큰
; https://www.sysnet.pe.kr/2/0/12424




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/15/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-04-03 11시45분
What is this [uuid(…)] in front of my C++ class declaration?
; https://devblogs.microsoft.com/oldnewthing/20230331-00/?p=107998

permissive 옵션을 켜면, Visual C++의 비표준 확장을 사용할 수 없으므로 [uuid] 속성도 컴파일 오류가 발생한다고 합니다. 대신 __declspec을 사용하면 되는데, 이것 역시 비표준이지만 단지 표준 방식(2개의 밑줄로 시작하는 식별자)으로 확장된 것이므로 허용을 한다고.

What are the duck-typing requirements of _com_ptr_t?
; https://devblogs.microsoft.com/oldnewthing/20230508-00/?p=108156
정성태

... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12614정성태4/24/20219852개발 환경 구성: 570. C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성 [4]파일 다운로드1
12613정성태4/23/20218891.NET Framework: 1048. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (2) 관리 코드파일 다운로드1
12612정성태4/23/20219037.NET Framework: 1047. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (1) PInvoke파일 다운로드1
12611정성태4/22/20218239오류 유형: 711. 닷넷 EXE 실행 오류 - Mixed mode assembly is build against version 'v2.0.50727' of the runtime
12610정성태4/22/20218077.NET Framework: 1046. C# - 컴파일 시점에 참조할 수 없는 타입을 포함한 이벤트 핸들러를 Reflection을 이용해 구독하는 방법파일 다운로드1
12609정성태4/22/20219498.NET Framework: 1045. C# - 런타임 시점에 이벤트 핸들러를 만들어 Reflection을 이용해 구독하는 방법파일 다운로드1
12608정성태4/21/202110437.NET Framework: 1044. C# - Generic Host를 이용해 .NET 5로 리눅스 daemon 프로그램 만드는 방법 [9]파일 다운로드1
12607정성태4/21/20218990.NET Framework: 1043. C# - 실행 시점에 동적으로 Delegate 타입을 만드는 방법파일 다운로드1
12606정성태4/21/202113084.NET Framework: 1042. C# - enum 값을 int로 암시적(implicit) 형변환하는 방법? [2]파일 다운로드1
12605정성태4/18/20219008.NET Framework: 1041. C# - AssemblyID, ModuleID를 관리 코드에서 구하는 방법파일 다운로드1
12604정성태4/18/20217625VS.NET IDE: 163. 비주얼 스튜디오 속성 창의 "Build(빌드)" / "Configuration(구성)"에서의 "활성" 의미
12603정성태4/16/20218497VS.NET IDE: 162. 비주얼 스튜디오 - 상속받은 컨트롤이 디자인 창에서 지원되지 않는 문제
12602정성태4/16/20219701VS.NET IDE: 161. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 [1]
12601정성태4/15/20218793.NET Framework: 1040. C# - REST API 대신 github 클라이언트 라이브러리를 통해 프로그래밍으로 접근
12600정성태4/15/20218952.NET Framework: 1039. C# - Kubeconfig의 token 설정 및 인증서 구성을 자동화하는 프로그램
12599정성태4/14/20219711.NET Framework: 1038. C# - 인증서 및 키 파일로부터 pfx/p12 파일을 생성하는 방법파일 다운로드1
12598정성태4/14/20219792.NET Framework: 1037. openssl의 PEM 개인키 파일을 .NET RSACryptoServiceProvider에서 사용하는 방법 (2)파일 다운로드1
12597정성태4/13/20219873개발 환경 구성: 569. csproj의 내용을 공통 설정할 수 있는 Directory.Build.targets / Directory.Build.props 파일
12596정성태4/12/20219560개발 환경 구성: 568. Windows의 80 포트 점유를 해제하는 방법
12595정성태4/12/20219000.NET Framework: 1036. SQL 서버 - varbinary 타입에 대한 문자열의 CAST, CONVERT 변환을 C# 코드로 구현
12594정성태4/11/20218458.NET Framework: 1035. C# - kubectl 명령어 또는 REST API 대신 Kubernetes 클라이언트 라이브러리를 통해 프로그래밍으로 접근 [1]파일 다운로드1
12593정성태4/10/20219661개발 환경 구성: 567. Docker Desktop for Windows - kubectl proxy 없이 k8s 대시보드 접근 방법
12592정성태4/10/20219480개발 환경 구성: 566. Docker Desktop for Windows - k8s dashboard의 Kubeconfig 로그인 및 Skip 방법
12591정성태4/9/202112745.NET Framework: 1034. C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법 [2]파일 다운로드1
12590정성태4/9/20219242.NET Framework: 1033. C# - .NET 4.0 이하에서 Console.IsInputRedirected 구현 [1]
12589정성태4/8/202110571.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법 [1]
... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...