型特性ライブラリ:std::is_base_of

std::is_base_of を説明するという課題で、Type-Traits ライブラリに関する前回の記事を終了しました。 そして std::is_convertible 機能。本日、Helmut Zeisel 氏からの回答をご紹介できることを嬉しく思います。


Zeisel 氏の回答を紹介する前に、簡単にもう一度挑戦したいと思います。

私の挑戦

型特性関数の 2 つの実装について説明してください std::is_base_o f と std::is_convertible .

  • std::is_base_of
    namespace details {
     template <typename B>
     std::true_type test_pre_ptr_convertible(const volatile B*);
     template <typename>
     std::false_type test_pre_ptr_convertible(const volatile void*);
     
     template <typename, typename>
     auto test_pre_is_base_of(...) -> std::true_type;
     template <typename B, typename D>
     auto test_pre_is_base_of(int) ->
     decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));
    }
     
    template <typename Base, typename Derived>
    struct is_base_of :
     std::integral_constant<
     boolean,
     std::is_class<Base>::value && std::is_class<Derived>::value &&
     decltype(details::test_pre_is_base_of<Base, Derived>(0))::value
     > { };
    

      • std::is_convertible
    namespace detail {
     
    template<class T>
    auto test_returnable(int) -> decltype(
     void(static_cast<T(*)()>(nullptr)), std::true_type{}
    );
    template<class>
    auto test_returnable(...) -> std::false_type;
     
    template<class From, class To>
    auto test_implicitly_convertible(int) -> decltype(
     void(std::declval<void(&)(To)>()(std::declval<From>())), std::true_type{}
    );
    template<class, class>
    auto test_implicitly_convertible(...) -> std::false_type;
     
    } // namespace detail
     
    template<class From, class To>
    struct is_convertible : std::integral_constant<bool,
     (decltype(detail::test_returnable<To>(0))::value &&
     decltype(detail::test_implicitly_convertible<From, To>(0))::value) ||
     (std::is_void<From>::value && std::is_void<To>::value)
    > {};
    

    確かに、かなり単純な課題があります。したがって、std::is_base_of に対して非常に良い答えが 1 つしか得られませんでした。 .しかし、Zeisel 氏による次の説明は非常に有益であるため、検討する価値があります。私は彼のドイツ語の説明を英語に翻訳し、彼のレイアウトを維持しました.

    std::is_base_of

    Program1.cpp

    std::is_base_of 基本的に、https://en.cppreference.com/w/cpp/language/overload_resolution などにある C++ 関数オーバーロード解決ルールの詳細に基づいています。例えば。これで使用される最初の規則は次のとおりです。

    この例は Program1.cpp です

    // Program1.cpp
    
    #include <iostream>
    struct Base {};
    struct Derived : public Base {};
    struct A {};
    // Conversion that converts pointer-to-derived to pointer-to-base
    // is better than the conversion of pointer-to-derived to pointer-to-void,
    // https://en.cppreference.com/w/cpp/language/overload_resolution
    void f(void*)
    {
     std::cout << "f(void*)" << std::endl;
    }
    void f(const Base*)
    {
     std::cout << "f(Base*)" << std::endl;
    }
    int main()
    {
     Derived d;
     A a;
     f(&d);
     f(&a);
     return 0;
    }
    

    出力は

    f(base*)
    f(void*)
    

    Program2.cpp


    この規則は、派生クラスへのポインターを別のポインターと区別するために使用できます。これから、型特性は Program2.cpp のように構築できます:

    // Program2.cpp
    
    #include <iostream>
    namespace details
    {
     template <typename B>
     std::true_type test_pre_ptr_convertible(const volatile B *);
     template <typename>
     std::false_type test_pre_ptr_convertible(const volatile void *);
    }
    template <typename Base, typename Derived>
    struct is_base_of : std::integral_constant<
     bool,
     std::is_class<Base>::value && std::is_class<Derived>::value &&
     decltype(details::test_pre_ptr_convertible<Base>
     (static_cast<Derived *>(nullptr)))::value
     > { };
    struct Base {};
    struct Derived : public Base {};
    struct A {};
    int main()
    {
     std::cout << std::boolalpha;
     std::cout << "Base is base of Derived: "
     << is_base_of<Base, Derived>::value << "\n";
     std::cout << "Derived is base of Base: "
     << is_base_of<Derived, Base>::value << "\n";
     std::cout << "Base is base of A: "
     << is_base_of<Base, A>::value << "\n";
     std::cout << "Base is base of Base: "
     << is_base_of<Base, Base>::value << "\n";
     std::cout << "Base is base of const Derived: "
     << is_base_of<Base, const Derived>::value << "\n";
     std::cout << "int is base of int: "
     << is_base_of<int, int>::value << "\n";
     std::cout << "void is base of void: "
     << is_base_of<void, void>::value << "\n";
     std::cout << "void is base of Base: " < < is_base_of<void, Base>::value << "\n";
     return 0;
    }
    

    test_pre_ptr_convertible は、引数の型と戻り値の型が異なる 2 つの関数です。関数は単純に宣言されています。関数本体が実際に呼び出されることはないため、関数本体の実装は必要ありませんが、コンパイル時にのみ戻り値の型が照会されます:test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr) . Derived の場合 実際には Base から派生しています 、関数 test_pre_ptr_convertible(const volatile B*) 戻り値の型が std::true_type の場合 選択されています。戻り値の型は decltype で決定されます 型に関連付けられた静的変数値の値は true です . Derived の場合 Base から派生したものではありません 、関数 test_pre_ptr_convertible(const volatile volatile*) 戻り値の型が std::false_type の場合 が選択され、対応する静的変数値の値は false です .
    const volatile const が必要です Derived または volatile Derived base から派生したものとして認識できます .実装では、クラスもそれ自体のベースと見なされるため、is_base_of<base,base> true を返します .
    派生はクラスに対してのみ意味があるため、以下が使用されます std::is_class<Base>::value && std::is_class<Derived>::value そのため、例えば。 is_base_of<int,int>::value false を返します .

    Program3.cpp

    一見すると、Program2.cpp は、本来あるべきことを既に行っているように見えます。ただし、C++ は多重継承をサポートしています。したがって、基底クラスが派生階層で複数回発生する可能性があります。これは Program3.cpp でテストできます:

    // Program3.cpp
    
    #include <iostream>
    namespace details
    {
     template <typename B>
     std::true_type test_pre_ptr_convertible(const volatile B *);
     template <typename>
     std::false_type test_pre_ptr_convertible(const volatile void *);
    }
    template <typename Base, typename Derived>
    struct is_base_of : std::integral_constant<
     bool,
     std::is_class<Base>::value &&
     std::is_class<Derived>::value &&
    decltype(details::test_pre_ptr_convertible<Base>
    (static_cast<Derived *>(nullptr)))::value
    >{ }; struct Base {}; struct Derived1 : public Base {}; struct Derived2 : public Base { }; struct Multi : public Derived1, public Derived2 { }; int main() { std::cout << std::boolalpha; // error: ‘Base’ is an ambiguous base of ‘Multi’ std::cout << "Base is base of Multi: " << is_base_of<Base, Multi>::value << "\n"; return 0; }

    コンパイラがエラー メッセージを返すようになりました
    error: 'Base' is an ambiguous base of 'Multi'

    Program4.cpp


    再び明確にするために、SFINAE と余分なレベルの間接化 (関数 test_pre_is_base_of の形式) ) は便利です:

    // Program4.cpp
    
    #include <iostream>
    namespace details
    {
     template <typename B>
     std::true_type test_pre_ptr_convertible(const volatile B *);
     template <typename>
     std::false_type test_pre_ptr_convertible(const volatile void *);
     template <typename, typename>
     auto test_pre_is_base_of() -> std::true_type;
     template <typename B, typename D>
     auto test_pre_is_base_of() -> decltype(test_pre_ptr_convertible<B>(static_cast<D *>(nullptr)));
    }
    template <typename Base, typename Derived>
    struct is_base_of : std::integral_constant<
     bool,
     std::is_class<Base>::value && 
     std::is_class<Derived>::value && 
     decltype(details::test_pre_is_base_of<Base, Derived>())::value
    > {}; struct Base {}; struct Derived1 : public Base {}; struct Derived2 : public Base {}; struct Multi : public Derived1, public Derived2 {}; int main() { std::cout << std::boolalpha; std::cout << "Base is base of Multi: " << is_base_of<Base, Multi>::value << "\n"; // error: call of overloaded ‘test_pre_is_base_of<Derived2, Multi>()’ // is ambiguous // std::cout << "Base is base of Derived1: " //<< is_base_of<Base, Derived1>::value << "\n"; return 0; }

    関数呼び出しの場合
    test_pre_is_base_of<base,multi>()
    2 つの関数
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

    そして
    template<typename, typename>.
    auto test_pre_is_base_of() -> std::true_type;

    選べます。関数呼び出し
    test_pre_ptr_convertible<base>(static_cast<multi*>(nullptr))
    通話
    test_pre_ptr_convertible(const volatile Base*);
    。しかし、Multi の 2 つの基数のどちらかが明確でないため、これはあいまいです。 ポインター Base *を指す必要があります。したがって、これは「置換失敗」を引き起こします。しかし、「置換の失敗」は「エラー」ではないため、他の関数
    template <typename, typename>
         auto test_pre_is_base_of() -> std::true_type;

    チェックされています。これは有効なので、
    decltype(details::test_pre_is_base_of<base,multi>())::value を返します
    このパスを介して値 true を返します。
    残念ながら、この型特性は単純な基本クラスでは機能しなくなりました
    is_base_of<base,derived1>::value
    この場合は両方の関数
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

    そして
    template<typename, typename>
      auto test_pre_is_base_of() -> std::true_type;

    Function Overload Resolution ルールに従って有効で同等です。したがって、この問題を解決するには、まずそれを何とか強制する必要があります
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

    が選択され、
    template <typename, typename>
        auto test_pre_is_base_of() -> std::true_type;

    最初の関数が置換失敗を返す場合にのみ選択されます。

    Program5.cpp


    これにも解決策があります。「標準の変換シーケンスは、ユーザー定義の変換シーケンスや省略記号の変換シーケンスより常に優れています。」

    // Program5.cpp
    
    #include <iostream>
    namespace details
    {
     template <typename B>
     std::true_type test_pre_ptr_convertible(const volatile B *);
     template <typename>
     std::false_type test_pre_ptr_convertible(const volatile void *);
     template <typename, typename>
     auto test_pre_is_base_of(...) -> std::true_type;
     template <typename B, typename D>
     auto test_pre_is_base_of(int) -> decltype(test_pre_ptr_convertible<B>(static_cast<D *>(nullptr)));
    }
    // A standard conversion sequence is always better
    // than a user-defined conversion sequence
    // or an ellipsis conversion sequence.
    // https://en.cppreference.com/w/cpp/language/overload_resolution
    template <typename Base, typename Derived>
    struct is_base_of : std::integral_constant<
     bool,
     std::is_class<Base>::value && std::is_class<Derived>::value &&
    decltype(details::test_pre_is_base_of<Base, Derived>(0))::value
    > {}; struct Base {}; struct Derived1 : public Base {}; struct Derived2 : public Base {}; struct Multi : public Derived1, public Derived2{}; int main() { std::cout << std::boolalpha; std::cout << "Base is base of Derived1: " << is_base_of<Base, Derived1>::value << "\n"; std::cout << "Derived1 is base of Base: " << is_base_of<Derived1, Base>::value << "\n"; std::cout << "Base is base of Derived2: " << is_base_of<Base, Derived2>::value << "\n"; std::cout << "Derived2 is base of Base: " << is_base_of<Derived2, Base>::value << "\n"; std::cout << "Derived1 is base of Multi: " << is_base_of<Derived1, Multi>::value << "\n"; std::cout << "Derived2 is base of Multi: " << is_base_of<Derived2, Multi>::value << "\n"; std::cout << "Base is base of Multi: " << is_base_of<Base, Multi>::value << "\n"; return 0; }


    template <typename B, typename D>
        auto test_pre_is_base_of(int) ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));
    を使用する場合
    (つまり、int への「標準変換」 )、
    template <typename, typename>
        auto test_pre_is_base_of(...) -> std::true_type;

    (つまり、「省略記号」) の場合、最初の関数 (標準変換) が優先的に選択され、2 番目の関数 (省略記号) は実際には SFINAE の場合にのみ選択されます。したがって、型特性は複数の基本クラスと単純な基本クラスの両方で機能します。

    次は?

    型特性ライブラリを使用すると、型を確認または比較できるだけでなく、型を変更することもできます。これはまさに私の次の記事で扱うものです.