1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ATLを使うレガシープロジェクトを最新のMSVCにしたら躓いた話

1
Posted at

Visual Studio 2010 時代に書かれたプロジェクトを Visual Studio 2026 でビルドできるようにしたところ、ATLが思う通りに動かず振り回されたという話です。原因を追っていくと、見慣れないものがあって面白かったので記事にしてみました。_ATL_MODULES とか __if_exists とかの話です。

令和の時代にするような話ではないかもしれませんが、同じようなレガシー移行をする必要がある人、あるいは古い技術に思いを馳せたい人への参考になれば幸いです。

COM や ATL って?

簡単に書くと、COM はかつて Windows で広く使われていた技術で、ATL はそれを扱いやすくするための C++ ライブラリです。COM を今から新規に採用することはほぼないと思いますが、Windows の内部では今でも多くの COM コンポーネントが使われています。

ATL (Active Template Library) はその名の通り C++ のテンプレートを使ったライブラリで、CComObject<T> や CComPtr<T> などのヘルパークラスを提供し、COM オブジェクトの実装や利用をしやすくします。

問題となったコード

コード例として、Greet() メソッドを持つ IGreeter というインターフェースを定義し、それを実装する CMyGreeter というクラスを作ります。利用側ではそれを CComObjectGlobal で包み、グローバルな COM オブジェクトとして使います。

COM や ATL に詳しくなくても、コード自体は雰囲気で何となく分かるかなと思います。

// COMインターフェース
struct __declspec(uuid("01234567-89AB-CDEF-0123-456789ABCDEF")) IGreeter : public IUnknown {
    virtual HRESULT STDMETHODCALLTYPE Greet() = 0;
};

// リソースID (通常はウィザードで生成)
#define IDR_MY_GREETER 101

// COMオブジェクト
class ATL_NO_VTABLE CMyGreeter : public CComObjectRootEx<CComSingleThreadModel>, public IGreeter {
public:
    BEGIN_COM_MAP(CMyGreeter)
    COM_INTERFACE_ENTRY(IGreeter)
    END_COM_MAP()

    DECLARE_REGISTRY_RESOURCEID(IDR_MY_GREETER)

    HRESULT FinalConstruct() {
        std::cout << "FinalConstruct" << std::endl;
        return S_OK;
    }

    void FinalRelease() {
        std::cout << "FinalRelease" << std::endl;
    }

    STDMETHOD(Greet)() override {
        std::cout << "Hello" << std::endl;
        return S_OK;
    }
};

// グローバルCOMオブジェクト
CComObjectGlobal<CMyGreeter> g_global_greeter;

// 利用側
int work() {
    IGreeter* greeter = nullptr;
    HRESULT hr = g_global_greeter.QueryInterface(__uuidof(IGreeter), reinterpret_cast<void**>(&greeter));
    if (SUCCEEDED(hr)) {
        greeter->Greet();   // "Hello" が出力される
        greeter->Release();
    }
}

コンパイラの移行で問題になったのは FinalConstruct や FinalRelease のところです。これらは、メソッドが定義されていれば COM オブジェクトの構築/破棄のタイミングで呼んでくれるというものですが、 CComObjectGlobal 経由で使用した時はこれが呼ばれず、初期化時に必要な処理が行われていませんでした。

※コード例で書くと不具合があればすぐに気付けそうに見えますが、実際はもっと大変でした。実際のコードはもっとごちゃごちゃしていますし、他の問題もあったりしたので。

CComObjectGlobal の実装

CComObjectGlobal のコンストラクタの実装を見てみます。そもそも「FinalConstructが定義されていればそれを呼ぶ」というのをどう実装しているのでしょうか?

Visual Studio 2026 付属の ATL では、以下のように実装されていました。

#ifdef _ATL_MODULES
template <class T, class = void>
struct _Has_FinalConstruct : ::std::false_type{};
template <class T>
struct _Has_FinalConstruct<T, ::std::enable_if_t<::std::is_assignable_v<HRESULT&, decltype(::std::declval<T>().FinalConstruct())>>> : ::std::true_type{};
#endif // _ATL_MODULES

template <class Base>
class CComObjectGlobal :
    public Base
{
public:
    typedef Base _BaseClass;
    template <class _Ty = CComObjectGlobal>
    CComObjectGlobal(_In_opt_ void* = NULL)
    {
        m_hResFinalConstruct = S_OK;
#ifdef _ATL_MODULES
        if constexpr (_Has_FinalConstruct<_Ty>::value)
        {
            if constexpr (_Has_InternalFinalConstructAddRef<_Ty>::value)
            {
                static_cast<_Ty&>(*this).InternalFinalConstructAddRef();
            }
            m_hResFinalConstruct = static_cast<_Ty&>(*this)._AtlInitialConstruct();
            if (SUCCEEDED(m_hResFinalConstruct))
                m_hResFinalConstruct = static_cast<_Ty&>(*this).FinalConstruct();
            if constexpr (_Has_InternalFinalConstructRelease<_Ty>::value)
            {
                static_cast<_Ty&>(*this).InternalFinalConstructRelease();
            }
        }
#else // ^^ _ATL_MODULES / !_ATL_MODULES vv
        __if_exists(FinalConstruct)
        {
            __if_exists(InternalFinalConstructAddRef)
            {
                InternalFinalConstructAddRef();
            }
            m_hResFinalConstruct = _AtlInitialConstruct();
            if (SUCCEEDED(m_hResFinalConstruct))
                m_hResFinalConstruct = FinalConstruct();
            __if_exists(InternalFinalConstructRelease)
            {
                InternalFinalConstructRelease();
            }
        }
#endif // _ATL_MODULES
    }
};

_ATL_MODULES というマクロで分岐していますね。後ほど触れますが、このマクロは ATL の実装を切り替えるためのものです。

個人的にはちょっと意外でしたが、このマクロが定義されている場合は std::enable_if_t や if constexpr を使うようです。ATLは既にメンテナンスモードであり、実装も以前のままだと思っていたのですが、比較的モダンな機能 (if constexpr は C++17 以降) を使って書き直されていました。

そして、マクロが定義されていない場合の実装。こちらは __if_exists という、見慣れないものを使っています。これは Microsoft の独自拡張の機能で、今回はこれが悪さをしているようでした。

答えを先に言うと、 _ATL_MODULES を定義した上で、他のいくつかの部分を直せば問題は解決しました。ですが、問題を理解するために __if_exists について更に追ってみます。

__if_exists について

名前の通り、指定された識別子が存在するかどうかをコンパイル時にチェックする機能のようです。これについては以下のページに説明があります。

説明には「テンプレートでは使わず単純な型にのみ使え」という注意書きがあります。

Apply the __if_exists statement to only simple types, not templates.

ATL の実装が思いっきりこれじゃないですか…

更に調べると、以下の Microsoft のブログに行き着きました。

Raymond Chen が2019年に投稿したもので、記事名は「Visual Studio の拡張キーワード __if_exists の悲しい歴史」というもの。冒頭で「いかなる場合も __if_exists を使うべきではない」と強調しつつ、ATL が作られた1996年当時を振り返りながら __if_exists の歴史を綴っています。

検証コード

__if_exists は問題があるとはいっても、古いコンパイラでは元のコードが問題なく動いていたはずです。この違いを検証するため、このキーワードとテンプレートを組み合わせた際の動作を確認する簡単なコードを書いてみました。

CComObjectGlobal の実装に近くなるよう、継承クラスをテンプレート引数にとるような基底クラスを書きます。

#include <iostream>

template<class Base>
class Derived : public Base {
public:
    // my_value が定義されていればそれを呼んで返す、
    // そうでなければ 0 を返す
    int get_value() {
        __if_exists(my_value) {
            return my_value();
        }
        return 0;
    }
};

class MyClass {
public:
    int my_value() {
        return 1;
    }
};

int main() {
    Derived<MyClass> instance;
    int v = instance.get_value();
    std::cout << "value: " << v << std::endl;
}

このコードを動かしてみると、 Visual Studio 2010 でビルドした場合は 1、 Visual Studio 2022 や 2026 でビルドした場合は 0 が出力されました。この違いが今回の現象の原因になっているようです。

この違いがどのコンパイラバージョンから生じているのか、という検証はしていません。 Visual Studio 2013 や 2015 は PC にインストールしていないですし、この問題のためにわざわざインストールしてまで調べようとは思わないので。少なくとも最新の MSVC では正しく動かない、というのは確かなようです。

_ATL_MODULES マクロについて

CComObjectGlobal の実装にあった _ATL_MODULES マクロについて見てみます。

このマクロは、非標準のC++拡張を無効化する /permissive- オプションを ATL プロジェクトで使えるようにするためのものとあります。ATLの実装が __if_exists を使わないものに置き換わるので、確かにその通りの動作です。

残念なことに「最新のコンパイラではこのマクロを使わないと問題が生じる可能性がある」といった不具合への記述は見当たりませんでした。レガシーな技術なので仕方ないし、今だに ATL のサポートを残してくれているだけでもありがたいと思うべきかもしれません。

ところで、 _ATL_MODULES が有効な場合の CComObjectGlobal の実装では C++17 の構文 (if constexpr) を使っていました。Visual Studio でプロジェクトを作成した際のデフォルトの言語バージョンは、2026 では C++20 になったものの、 2022 では C++14 です。そのため、2022 では明示的に言語バージョンを上げないと _ATL_MODULES 付きのビルドは失敗すると思います。

その他の問題

_ATL_MODULES を定義すれば問題は解決!…といきたいところですが、実際はもう少し変更が必要でした。このマクロが定義されていると、 DECLARE_REGISTRY_RESOURCEID が消えるようです。

class ATL_NO_VTABLE CMyGreeter : public CComObjectRootEx<CComSingleThreadModel>, public IGreeter {
public:
    ...
    DECLARE_REGISTRY_RESOURCEID(IDR_MY_GREETER)  // コンパイルエラー
}

ATLの実装を見ると、_ATL_MODULES が定義されている場合は代わりに DECLARE_REGISTRY_RESOURCEID_WITH_MODULE または DECLARE_REGISTRY_RESOURCEID_WITHOUT_MODULE を使うようです。

#ifdef _ATL_MODULES
// Note that the following implementation of DECLARE_REGISTRY_RESOURCEID removes the uses of __if_exists, but in a non-Standard
//        way (through taking the address of a member function without specifying the name of the class). This code will NOT
//        compile under clang-cl, but takes advantage of a non-Standard extension in MSVC to deduce the name of the enclosing
//        class. Whenever possible, users instead should use DECLARE_REGISTRY_RESOURCEID_V2 above which takes the class name
//        as a parameter rather than using this non-Standard extension to determine this information.
// Similarly, we cannot detect whether the global variable _Module has been declared or not in a Standard way (without the
//        use of __if_(not_)?exists), so we declare two versions of each macro: one where a _Module global variable has been
//        declared by the user that we use, and one where we use ATL::_pAtlModule instead.
#define DECLARE_REGISTRY_RESOURCEID_WITH_MODULE(x)\
    ...

#define DECLARE_REGISTRY_RESOURCEID_WITHOUT_MODULE(x)\
    ...

#else // ^^ _ATL_MODULES / !_ATL_MODULES vv
#define DECLARE_REGISTRY_RESOURCEID(x)\
    ...

DECLARE_REGISTRY_RESOURCEID_WITH_MODULE や DECLARE_REGISTRY_RESOURCEID_WITHOUT_MODULE は、ドキュメントに書かれていない上に Web で検索しても全くヒットしなかった 1 ので、ATLの実装を直接読まないと知り得ないものでした。

この2つの使い分けはコメントにある通り、 _Module というグローバル変数があるかどうかで使い分けるようです。また、可能であれば利用者は DECLARE_REGISTRY_RESOURCEID_V2 の方を使うべき (ただし既存の DECLARE_REGISTRY_RESOURCEID とは引数が異なるため互換はない) とあります。ここは実際にコードに合わせて適切なものを選びます。

おわりに

COMやATLのようなレガシーな技術はなかなか情報がなくて苦労します。特に今回、表面上は ATL の規約に従っているコードだったので、原因がライブラリ側だと気付くまでに時間がかかりました。こういうときにライブラリの実装を読んだり、検証用の小さいコードを書いたりするというのは、今でも大事だなと思います。

__if_exists は初めて知ったのですが、こういうものをテンプレートと組み合わせるのは確かに問題が起こりそうだと感じます。ATLは古いコードベースであるものの、C++ 17 の機能を使って __if_exists を置き換える実装があって、これはちょっと意外な感じがしました。

こういうものに触れる機会があるのは C++ だよなあ、と思います。

  1. この記事を書いている時点では、Google で検索してもヒット0件という状況でした。 ↩

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?