Help us understand the problem. What is going on with this article?

【C++】CRTPによる静的インターフェースクラスはライブラリ実装において不適切+解決法

More than 3 years have passed since last update.

過去の記事で静的ポリモーフィズムに触れた際に、静的インターフェースクラスの実装の例を挙げていた。この記事について一つ誤解が生じ得ないために修正を考えていたが、テーマが大きくなりすぎるために独立して記事をつくることにする。

CRTPによる静的インターフェースクラスとは

CRTP(Curiously Reccursive/Reccuring Template Pattern)とは、再帰的な定義がされるC++Idiomの一種である。

基本クラスが派生クラスの型をテンプレート引数として受け取る実装を指す。

template<class T>
class Base;

class Derived : Base<Derived>;

これを利用して、過去の記事で静的インターフェースクラスの実装の例を示した。
静的ポリモーフィズムの安全で簡単な実装 -動的から静的にしてパフォーマンス向上-

リンクをわざわざ踏んでいただくのも申し訳ないので、簡単にしたものを再掲する。

template<class T>
class Interface{
public:
   void function(){ static_cast<T&>(this)->function(); }
};

class Derived1 : Interface<Derived1>{
public:
    void function(){ std::cout << "Derived1" << std::endl; }
};

class Derived2 : Interface<Derived2>{
public:
};

見て頂いたらわかるように、thisポインタを静的キャストして目的のインターフェースを呼び出すことで、キャストの成功可否でインターフェースクラスを実現している。

これは純粋仮想関数を用いた場合以下のように書ける。

class Interface{
public:
   virtual void function()=0;
   virtual ~Interface()=default;
};

class Derived1 : Interface{
public:
    void function(){ std::cout << "Derived1" << std::endl; }
};

class Derived2 : Interface{
public:
};

この二つのコードの意味は"ほぼ"同一だが、やっていることはまるで違う。それは、前回説明したので割愛するとして

"ほぼ"と表現した意味を説明していく。

静的インターフェースクラスのエラーの性質

この二つのコードをそれぞれ使い、コンパイルしてみよう。

int main(){
    Derived1 a;
    Derived2 b;

    a.function();
    b.function();
    return 0;
}

動的実装の方からエラーを見ていく。

main.cpp:35:15: error: variable type 'Derived22' is an abstract class
    Derived22 b;
              ^
main.cpp:20:17: note: unimplemented pure virtual method 'function' in 'Derived22'
   virtual void function()=0;

純粋仮想関数が定義されていないことを簡潔に示していて読みやすい。

次に静的実装の方のエラーを見る。

main.cpp:6:21: error: non-const lvalue reference to type 'Derived2' cannot bind to a temporary of type 'Interface<Derived2> *'
   void function(){ static_cast<T&>(this)->function(); }
                    ^               ~~~~
main.cpp:23:7: note: in instantiation of member function 'Interface<Derived2>::function' requested here
    b.function();
      ^
main.cpp:6:42: error: member reference type 'Derived2' is not a pointer; maybe you meant to use '.'?
   void function(){ static_cast<T&>(this)->function(); }

さて、先ほどに比べて見辛いエラーだがテンプレートのエラーにしては良心的だし、誰でもfunctionが定義されていないことに怒られていることはわかるだろう。

ここまでは問題ないように思える。
だが、ここで実行するコードを以下のように変更してみよう。

int main(){
    Derived1 a;
    Derived2 b;

    a.function();
    //b.function();
    return 0;
}

不完全な型であるbのfunctionを呼ばないことにした。

これをコンパイルすると、動的実装のエラーは変わりなくfunction未定義のエラーを吐くが...

静的実装のコードはエラーを吐かない。

キャストに失敗するはずなのにコンパイルが通る理由

C++draftなどにも書いてあるが
N3337
N3337(Ezoe氏による和訳)

14.6のテンプレート実体化の記述あたりを見ればわかるが

クラステンプレートは使われないメンバは実体化しない。

今回の場合、不完全な型Derived2をインスタンス化した際、それに伴うInterface<Derived2>もインスタンス化されるが、そのメンバ関数functionは使用されていないために定義されない。

これは規格で定められており、テンプレート要素が肥大化されないための処理である。

後者のコードで静的実装がエラーを吐くためには

1.明示的インスタンス化
2.-Jaオプションによりすべてのメソッドをインスタンス化

などが考えられるが、共に不適切である。
template class Interface<Derived2>;
のように明示的なインスタンス化を行うのはあくまでユーザー側で、ライブラリの責任範囲を侵食してしまっている上、後者はそもそもこの仕様が非常に強力なものであるのにも関わらず無効化するのは論外なのである。

解決法

先に言い訳するとクソ適当である

コンパイル時に型特性を評価したいのなら、static_assertを使えばいいじゃないかという発想

静的インターフェースクラスにコンストラクタを用意し以下のように定義する

template<class T>
class Interface{
public:
    Interface(){
        static_assert(std::is_same<decltype(std::declval<T>().function()),void>::value,"function is not defined");
    }
};

一般化すると

static_assert(std::is_same<decltype(std::is_same<std::declval<T>().関数名(std::declval<引数1>(),
                                                                         ...,
                                                                         std::declval<引数n>()),
                                                 返り値の型>::value,
              "迫真怒りのメッセージ");

これで静的にシグニチャが完全かどうか検査できる。

たとえばエラーを吐くケースとして返り値が違う例を示す。

#include<iostream>
#include<utility>

template<class T>
class Interface{
public:
    Interface(){
        static_assert(std::is_same<decltype(std::declval<T>().add(std::declval<int>(),std::declval<int>())),int>::value,"add is not defined");
    }
};

class Derived1 : Interface<Derived1>{
public:
    double add(int a,int b){return a+b;}
};

int main(){
    Derived1 a;
    return 0;
}

インターフェースクラスが要求しているシグニチャはint(int,int)なのにも関わらず、実装がdoubleになっているため、このコードはコンパイルエラーとなる。

エラーコードは以下のようになる。

main.cpp:8:9: error: static_assert failed "add is not defined"
        static_assert(std::is_same<decltype(std::declval<T>().add(std::declval<int>(),std::declval<int>())),int>::value,"ad...
        ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:12:7: note: in instantiation of member function 'Interface<Derived1>::Interface' requested here
class Derived1 : public Interface<Derived1>{

依然クラステンプレート自体は実体化しなくてはならない問題が残っているが、すべてのインターフェースを使ってみなくては型検査できなかった頃に比べればだいぶマシになった方であろう。

ちなみにいらんクラス作ってオーバーヘッドは大丈夫なのか、という人もいるだろう。

最適化を一切かけなければ当然Interfaceコンストラクタは呼ばれ、少しオーバーヘッドを生むことになる。

だが、最適化をかけてアセンブリコードを見てみよう。

class Derived1 : Interface<Derived1>{
public:
    int _a;
    Derived1(){ std::cin >> _a; }
    int add(int a, int b){ return a + b; }
};

ちょこっとコンストラクタでなんかするようにして、アセンブリコードを見てみると

int main(){
002A12A0  push        ebp  
002A12A1  mov         ebp,esp  
002A12A3  sub         esp,8  
002A12A6  mov         eax,dword ptr ds:[002A4000h]  
002A12AB  xor         eax,ebp  
002A12AD  mov         dword ptr [ebp-4],eax  
    Derived1 a;
002A12B0  mov         ecx,dword ptr ds:[2A3030h]  
002A12B6  lea         eax,[a]  
002A12B9  push        eax  
002A12BA  call        dword ptr ds:[2A3038h]  
    a.add(1, 1);
    return 0;
}

Interfaceはなかったことになっているのが分かる。

というわけで問題ないと言える、と思われる。

6/21 追記 真面目な解決法

真面目に考えました。

テンプレートメソッドは呼び出さないと実体化されないと言ったが、厳密には嘘だ。
メンバ関数ポインタを定義すれば実体化される。

template <class T> inline void ignore_unused_variable_warning(T const&) {}
template <class T>
void functionRequires()
{
    void (T::*fptr)() = &T::function;
    ignore_unused_variable_warning(fptr);
}

メンバ関数ポインタを取得する。こうすると、要求しているインターフェースfunctionを、呼び出さずとも実体化することができる。...って結局functionRequires呼ばなきゃいけないんじゃないかーい!って思った人。
テンプレートはポインタを渡せることを忘れていないだろうか。

template<class T>
class Interface{     
    void constraint() {
        T t;
        t.function();
    }

    typedef void (Interface<T>::* type)();
    template <type _Tp1>
    struct Check { };
    typedef Check<& Interface<T>::constraint> _Check;
};

こんな感じで、constraintメソッド(別にこれはなんでもいい)に好きなインターフェースの呼び出しを記述して、そのメンバ関数ポインタを静的に取得するだけ。一切オーバーヘッドはない。

こんな感じで、Interfaceに限らず色々な操作をconstraintでやれば、クラスに対して制約を与えることができる。

冗長なコードだからマクロを使いたくなる。マクロ好き。読む方はたまったもんじゃないが。

#define CLASS_REQUIRE(TT, NS, Concept) \
typedef void (NS::Concept <TT>::* func##TT##Concept)(); \
template <func##TT##Concept _Tp1> \
struct Concept_checking_##TT##Concept { }; \
typedef Concept_checking_##TT##Concept< \
& NS::Concept<TT>::constraint> \
Concept_checking_typedef_##TT##Concept

// デフォルトコンストラクタの定義を要求する制約
template<class TT>
struct DefaultConstructible {
        void constraint() {
            TT a;
            ignore_unused_variable_warning(a);
        }
    };

template<class T>
class Interface {
CLASS_REQUIRE(T, , DefaultConstructible);
};

class myclass : Interface <myclass> {};

...ってこれコンセプトイディオムじゃないかーーーーーーーーーーーいwwwwwwwwwwwww

終わり。

Riyaaaa_a
ゲームプログラマです。 Template meta programming / Real-time rendering 活動拠点をはてなに移しました。
https://riyaaaaasan.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away