66
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++Advent Calendar 2017

Day 20

C++テンプレートはコードジェネレータ

Last updated at Posted at 2017-12-22

#関数テンプレートは関数ではないし、クラステンプレートはクラスではない

今回はテンプレートがコードジェネレータ的な側面が強いよという話しをしたい。
関数テンプレートは関数ではない、関数テンプレートが関数を生成するのだ。
クラステンプレートはクラスではない、クラステンプレートがクラスを生成するのだ。
そういう話になる。

#コンパイル時に型や値をパラメタとして渡す機能としてのテンプレート

テンプレートの理解として、型をパラメタとして渡せる機能という考え方がある。

template < class T >
void func(T x) { return x; }

この関数テンプレートは引数がテンプレート化されているがこれを

func<int>(1);

のように型をパラメタとして渡して使える。

言ってしまえば、テンプレートは関数が引数をとるように型を変数化して後から指定できるようにしたというわけだ。

また、非型テンプレートというものもあり、型以外に値も渡せるということを述べておく。

template < size_t N >
constexpr auto func() { return std::array<int,N>{{}}; }

int main(){
    constexpr auto arr = func<4>();
}

#テンプレートのインスタンス化

##そのクラステンプレート、いつインスタンス化するの?

今でしょ?
いいえ、すぐインスタンス化されると困ります。

ところで、テンプレートを使うコードがあるとき、一体どれだけのテンプレートがインライン展開されているのかを疑問に思ったことはあるでしょう。
実はクラステンプレートの場合、インスタンス化はクラステンプレートの完全な定義が必要になったとき初めてインスタンス化され、それ以前までインスタンス化が遅延される。

次の例を見てくれ

template < class T > class C; // (1):宣言

C<int>* ptr = nullptr; // (2):ポインタの使用

template < class T >
class C{
public:
    T value; // (3):メンバの宣言
}; // (4):クラステンプレートの定義完了

void f(C<int>& c) // (5):宣言を使用
{
    auto v = c.value; // (6):クラステンプレートの定義を使用している
}

1の時点では宣言しか見えておらず、宣言の利用しかできない。
2のポインタの利用は定義を必要としないので可能。
3でメンバが定義されていることに注目。
4で定義が完了している
5は宣言だけなのでまだインスタンス化には至らない。
6はクラスのメンバにアクセスしている、これは定義の利用にあたる。

メンバにアクセスすると、メンバのアクセス指定子(private, public, protected)が関わることは容易に想像できるし、それはクラスの定義を見なければ分からない。

もう一つの定義要求はクラスのサイズに関わるものだ

auto ptr = new C<int>();

new演算子でヒープにインスタンスを確保するとなるとサイズがわからんくてはどうしょうもない。
これにはクラスの定義が必要になるのは自明の理だ。

#Two Phase Lookup

C++のテンプレートはTwo Phase Lookupというルールがあり、
2段階の検索によって行われるコンセプトになっている。
第1段階でテンプレートを構文解析し、第2段階でそれをインスタンス化する。

##その関数テンプレート、どこにインスタンス化されるの?

この2段階目のインスタンス化は**POI(Point Of Instantiation)**と呼ばれ、テンプレートパラメタを何らかの型に置き換えたコードがどこぞにインライン展開され、ADLが実行されることになる。

次のコード例を見てほしい。
コンパイラのお気持ちになって考えてほしいのだが、
f<Hoge>(hoge)を見たコンパイラはコードのどこにコードをインライン展開するのだろうか?

struct Hoge {
    int value;
    Hoge() = default;
    ~Hoge() = default;
    Hoge(int value) : value{value} {}
    operator bool() const { return value > 0; }
    friend Hoge operator-(Hoge hoge) { return {-hoge.value}; }
};

template < class T >
void f(T hoge)
{
    if(hoge){
        g(-hoge);
    }
}

void g(Hoge hoge){
    f<Hoge>(hoge);
}

まず、呼び出し位置から遠く離れた場所にインライン展開されることは考えにくい。
そしてC++では関数の中で関数を定義できないことを踏まえると
大体以下の場所だろうという気持ちになる

struct Hoge {
    int value;
    Hoge() = default;
    ~Hoge() = default;
    Hoge(int value) : value{value} {}
    operator bool() const { return value > 0; }
    friend Hoge operator-(Hoge hoge) { return {-hoge.value}; }
};

template < class T >
void f(T hoge)
{
    if(hoge){
        g(-hoge);
    }
}
// (1) ここか
void g(Hoge hoge){
    f<Hoge>(hoge);
}
// (2) ここ

(1)に展開すると不都合がある。
gの定義が見えんのだ。

よって、(2)に展開するのがお気持ちとなる。

まあそんなことはわからなくても良い。
このようにコンパイラはインスタンス化をなんか都合の良さげな位置にインスタンス化してくれる。

この点がテンプレートがコードジェネレータである主張する所以である。
通常の関数と違って関数テンプレートはインスタンス化されたコードがインライン展開されてどこぞの都合が良さげな位置に挿入されるわけだ。

##そのクラステンプレート、どこにインスタンス化されるの?

次の例をみてくれ
クラステンプレートのsizeofを要求されたことにより、クラステンプレートの完全な定義が必要になったコンパイラの気持ちになって考えてほしいのだが
クラスの定義をどこにインスタンス化すればいいのだろうか?

template < class T >
struct C {
    T mem_;
};

size_t hoge(){
    return sizeof(C<int>);
}

さっきの感じで行くと

template < class T >
struct C {
    T mem_;
};
// (1) ここか
size_t hoge(){
    return sizeof(C<int>);
}
// (2) ここ

ということになるだろう。
さっきと同様に(2)になるのではと思ったコンパイラの諸君もいるかもしれない。
もしそう思ったならば、まだまだ未熟なコンパイラなので縄文土器を作る特訓をしたほうが良い。

(2)にインスタンス化してしまうと、sizeofの場所から定義が見えないのでサイズがわからないのだ。
これはちょっとコンパイラのお気持ちになれば分かることである。

よって、(1)に展開するのがお気持ちとなる。

やっぱりそんなことはわからなくても良い。
クラステンプレートも関数テンプレートと同様コードジェネレータで、インスタンス化されたコードがインライン展開されてどこぞの都合が良さげな位置に挿入されるわけだ。

#テンプレートがコードジェネレータだからなんなの?

テンプレートとテンプレートでないものの決定的な違いは
分離コンパイルで現れる。

おそらくご存知だと思うが、テンプレートは宣言から定義まで全てがヘッダファイルに記述されることがほとんどである。

対して、テンプレートでないものはヘッダファイルに宣言ソースファイルに定義という分離コンパイルの手法が取られる場合も多い。

なぜこうも違いが出て来るのか、ここまで読み進めてくれたコンパイラの諸君はもうおわかりだろう。

テンプレートがコードジェネレータだからである!

真面目に説明しますね。

まず、典型的な分離コンパイルの関数を見ていく。

func.hpp
#include <iostream>
namespace ns {
    void func();
}
func.cpp
#include <iostream>
namespace ns {
    void func(){
        std::cout << "hoge" << std::endl;
    }
}
main.cpp
#include "func.hpp"

int main(){
   ns::func();
}
g++ main.cpp func.cpp && ./a.out

output

hoge

実行結果|Wandbox

まあ、このように定義をソースファイルに定義を書いて宣言だけヘッダで共有すると、リンク時にうまいこと解決されるわけだ。

単純にこれをテンプレートでやるとどうだろう?

func.hpp
#include <iostream>
namespace ns {
    template < class T >
    void func(T x);
}
func.cpp
#include <iostream>
namespace ns {
    template < class T >
    void func(T x){
        std::cout << x << std::endl;
    }
}
main.cpp
#include "func.hpp"

int main(){
   ns::func("hoge");
}
g++ main.cpp func.cpp && ./a.out

output

/tmp/ccWWV3GC.o: In function `main':
prog.cc:(.text+0xa): undefined reference to `void ns::func<int>(int)'
collect2: error: ld returned 1 exit status

実行結果|Wandbox

動かない。
理由は簡単で、関数テンプレートがコードジェネレータだからである!

一応説明しますよ。

ns::func("hoge")を見たコンパイラは関数を探すわけです。
しかしながら、func.hppには定義はなく、関数テンプレートの宣言しかありません。
func.cppには関数テンプレートの定義がありますが、コンパイル時には見えません。
もはや、コンパイラに為す術はありません。
関数テンプレートがインスタンス化されることは無く、``void ns::func(char const*)'`の解決に失敗し無残にエラーを吐いてしましました。

関数テンプレートを書いているC++erは99割が必要になったときに必要なだけ都合の良いインスタンス化ができてほしいみたいな思いがちょっとはあると勝手に思ってます。

必要最低限の関数が存在すれば良いのなら、普通の関数を書けば良いのです。
しかしながら関数テンプレートを使った別の方法がなくもないです。

##明示的な特殊化の利用

先程のテンプレートのコードの何がいけなかったかといえば、関数テンプレートがインスタンス化しなかったという一点である。
ならばソースファイルでインスタンス化までやってしまえば万事OKでござるな?

func.hpp
#include <iostream>
namespace ns {
    template < class T >
    void func(T x);
}
func.cpp
#include <iostream>
#include "func.hpp"

namespace ns {

    template < class T >
    void func(T x){
        std::cout << x << std::endl;
    }
}

template void ns::func<const char*>(const char*);

main.cpp
#include "func.hpp"

int main(){
   ns::func("hoge");
}
g++ main.cpp func.cpp && ./a.out

output

hoge

実行結果|Wandbox

これは明示的なテンプレートのインスタンス化とか言われてるやり方である。

template void ns::func<const char*>(const char*);

という部分がテンプレートのインスタンス化の文法である。
これを見たコンパイラはこう思う。
インスタンス化しなきゃ!

この方法の特徴は

明示的インスタンス化を書いたものしかインスタンス化しない

ということにある。
必要なものはインスタンス化を羅列しておかなくてはならない。
逆にいうと、意図しないインスタンス化を絶対にさせないという鉄の意志である(明示的インスタンス化を書く理由はこれが大きい)。

実体化されていない関数テンプレートは扱えない

テンプレートがコードジェネレータだという事実により、ジェネリクスとの違いが生まれる。
ジェネリクスのボックス化と違い、インライン展開されていないなら実態は存在し得ない。
よって、関数テンプレートは実体化しないと受け渡しできない。
C++において、関数は第一級オブジェクトではないのでこれはどうしようもない。

template < class T >
void func(const T& v){ std::cout << v << "\n"; }

template < class F, typename... Args >
decltype(auto) invoke(F&& f, Args&&... args){
  return std::forward<F>(f)(std::forward<Args>(args)...);
}

int main()
{
    invoke(func, 1); // error!

    invoke(func<int>, 1); // ok!
}

関数オブジェクト

関数が第一級オブジェクトでないのが問題なら、第一級オブジェクトであるクラスを使えばいい。
operator()を持つクラスである。
__関数オブジェクト__と呼ばれているものだ。

ラムダ式を使うこともできる。
C++14が使えるならジェネリックラムダを使ったほうがいい。

struct Functor {
    template < class T >
    void operator()(const T& v){ std::cout << v << "\n"; }
}
;
template < class F, typename... Args >
decltype(auto) invoke(F&& f, Args&&... args){
  return std::forward<F>(f)(std::forward<Args>(args)...);
}


int main()
{
    invoke(Functor{}, 1);
    invoke(Functor{}, "hoge");

    // ラムダ式を使うこともできる
    auto functor = [](auto v){ std::cout << v << "\n"; };
    invoke( functor, 1 );
    invoke( functor, "hoge" );
}

template template: 部分適用されてないクラステンプレートを扱う

クラスの場合はテンプレートパラメータを中抜きした状態で受け渡し可能。
__template template parameter__としてやり取りする。
template template例としてコンテナの指定がよく転がっている。
要素型は固定だが、コンテナを後で指定したいような場合だ。
これはtemplate templateを使うと実現できる。


template < template <class T, class A = std::allocator<T>> class Container >
class Hoge {
    Container<int> data;
public:
    // ...
};

int main(){
    Hoge<std::vector> hoge1; // 内部コンテナにvectorを指定
    Hoge<std::list> hoge2;   // 内部コンテナにlistを指定
}

このようにstd::vector<T,A>std::vectorのようにテンプレートパラメータが部分適用されていない形で受け渡しできる。

template < template <class> class TT >

における__TT__というのはテンプレートパラメータを一つもつクラステンプレートを意味する。

#まとめ

テンプレートはコードジェネレータ的な側面があるよね。

66
55
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
66
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?