Thumbnail image

C++ メタ関数の使い方

概要

はじめに

こんにちは、株式会社 Life Arc System の 開発担当の mzn です。

C++ は新しい規格になるにつれて、型推論やコンセプト、テンプレートパラメータの要件の緩和など、型に関する言語機能の追加や強化が行われてきました。
これにより、型生成やテンプレート引数に対する制約など、型に対する制御を記述しやすい環境になってきたと思われます。

上記のような、型に対する制御の記述はメタプログラミング1という、プログラミング技法に含まれます。
特に C++ のテンプレートの機能を利用して行うメタプログラミングが、テンプレートメタプログラミングと呼ばれます。

なお、テンプレートメタプログラミングを行う際には、メタ関数を使うと便利です。
本記事では C++20 の言語機能を対象としたメタ関数について説明させていただきます。

対象読者

本投稿は、以下のような方を対象として執筆しています。

  • C++ のメタプログラミングに興味がある方
  • クラスの静的メンバの呼び出し方をご存じの方
  • std::vectorstd::arrayなど、テンプレートクラスを使ったことがある方

環境

本投稿の内容は、以下の環境にて確認しています。

  • 言語 … C++ (C++20 の機能を有効にしてコンパイル)
  • コンパイラ … gcc-11、clang-14

メタ関数とは

C++ のメタ関数は、主に型を引数として受け取り2、下記のような結果を返します。

  1. 受け取った型に対して何かしらの判定を行い、真偽値を返却
  2. 受け取った型を元に新たな型を生成して返却

また、メタ関数はテンプレートクラスの言語機能を応用して定義されています
そのため、下記対応表のように、関数の機能をテンプレートクラスの機能に置き換えて考える必要があります。

関数の機能対応するテンプレートクラスの機能
関数名クラス名
引数テンプレートパラメータ
戻り値コンパイル時評価可能なメンバ定数valueまたは、メンバ型type
評価のタイミングは実行時3評価のタイミングはコンパイル時

なお、メタ関数はテンプレートがインスタンス化されることで結果が確定し、下記に格納されます。

  • value … 結果の値が格納される、コンパイル時評価可能なメンバ定数
  • type … 結果の型が格納される、メンバ型

クラスなのに関数…? と、直感的ではないメタ関数の定義に戸惑う方もいらっしゃるかと思います。
しかし、これは使ってみて慣れるしかありませんので、形式的な設計パターンのようなものと考え、戸惑いは飲み込んでください。

標準ライブラリのメタ関数

C++ の標準ライブラリでは基本的なメタ関数が<type_traits>ヘッダ4std名前空間内に定義されています。
一部となりますが、いくつかメタ関数を紹介させて頂きます。

is_same

メタ関数is_same5は、2 つの型を受け取って等しいか判定し、真偽値を返却します。
結果はコンパイル時評価可能なメンバ定数valueに格納されます。
例えば、下記のような比較を行うことができます。

  • int型とint型を比較 … 同じ型なので、結果は真
  • int型とfloat型を比較 … 異なる型なので、結果は偽

is_integral

メタ関数is_integral6は、1 つの型を受け取って整数型か判定し、真偽値を返却します。
結果はコンパイル時評価可能なメンバ定数valueに格納されます。
下記のように、整数に分類出来る型は真、できない型は偽となります。

  • 結果が真となる型の例 … intshortunsigned long
  • 結果が偽となる型の例 … voidfloatnullptr_t

conditional

メタ関数conditional7は三項演算子と同様に、条件に応じて結果を分岐できます。
結果はメンバ型typeに格納されます。
下記のように 3 つのテンプレートパラメータを受け取ります。

  • 1 つ目のテンプレートパラメータ … 真偽値を指定します。型ではなく、値を指定します。
  • 2 つ目のテンプレートパラメータ … 1 つ目のテンプレートパラメータが真と評価された場合、ここに指定された型がconditionalの結果として返却されます。
  • 3 つ目のテンプレートパラメータ … 1 つ目のテンプレートパラメータが偽と評価された場合、ここに指定された型がconditionalの結果として返却されます。

メタ関数の使い方

メタ関数とはでも言及させていただきましたが、下記の手順を踏むことでメタ関数の実行と結果の取得を行うことができます。

  1. メタ関数のテンプレートパラメータをすべて指定し、テンプレートをインスタンス化
  2. インスタンス化したメタ関数のメンバへアクセス

なお、以降のソースコードの中では、C++ の言語機能であるstatic_assertを利用して動きを確認しているため、先に説明させていただきます。
static_assertはコンパイル時にアサートを行います。
下記のように関数のように表記し、1 つ目のパラメータに真偽値として評価可能な定数式、2 つ目のパラメータに文字列リテラルを指定します。
文字列リテラルは省略可能です。

static_assert(<真偽値として評価可能な定数式>, <文字列リテラル>);

static_assertはコンパイル時に動作し、1 つ目のパラメータが真偽どちらに評価されるかによって以下の動作となります。

  • 偽と評価された場合 … その時点でコンパイルエラーとなり、指定した文字列リテラルをエラーメッセージとして出力する
  • 真と評価された場合 … 特になにも起こらず、そのままコンパイルを継続する

具体的に、static_assert(true)と記載した場合はなにも起こりませんが、以下のソースコードを記述した場合はコンパイルエラーが発生します。

// gcc コンパイラによるコンパイルエラー出力
// error: static assertion failed: Error occured!
// 
// clang コンパイラによるコンパイルエラー出力
// error: static_assert failed "Error occured!"
static_assert(false, "Error occured!");
int main() {}

本投稿におけるメタ関数の動作は、static_assertの 1 つ目のパラメータへ、コンパイルエラーが発生しないような判定式を記述することで、どのような結果となるかを確認しています。

それでは、実際にソースコードを見ながら、標準ライブラリのメタ関数で紹介させていただきましたメタ関数を用いて、使い方を確認してみましょう。

1.真偽値を返却するメタ関数

例として、is_sameにより 2 つの型が等しいかどうか判定します。
比較したい 2 つの型をテンプレートパラメータに指定し、メンバ定数valueを参照することでメタ関数の実行結果を確認しています。

#include <type_traits>

// 同じ型を指定することで真となることを確認
static_assert(std::is_same<int, int>::value == true);

// 異なる型を指定することで偽となることを確認
static_assert(std::is_same<int, float>::value == false);

// コンパイルを通すために空のエントリーポイントを実装
int main() {}

また、C++17 から、変数テンプレート8によるstd::is_same_vの定義が追加され、テンプレートパラメータを指定するだけで結果を取得することが可能となりました。
これにより、メンバ定数valueを参照する方法と比較して、より関数呼び出しに似ている書き方ができます。

#include <type_traits>

// is_same_v<...> と記述することにより、is_same<...>::value と同じ結果を取得可能
static_assert(std::is_same_v<int, int> == true);
static_assert(std::is_same_v<int, float> == false);

int main() {}

2.型を返却するメタ関数

例として、conditionalを用いて、必ず整数型が返却されるような処理を記述します。

is_integralにより、整数かどうか判定を行い、整数型の場合は判定に使用した型をそのまま、整数型以外の場合はintを返却するようにします。
conditionalのメンバ型typeに格納された型を、is_sameで比較することでメタ関数の実行結果を確認しています。

なお、下記ソースコードで出現するusingはエイリアス宣言という言語機能です。
変数宣言のような文法で、型に別名をつけることができ、宣言した別名は型として使うことができます。

#include <type_traits>

// unsigned long は整数型のため、type1 には unsigned long 型が格納される
using type1 = std::conditional<
    std::is_integral_v<unsigned long>,
    unsigned long,
    int
>::type;
static_assert(std::is_same_v<type1, unsigned long>);

// float は整数ではないため、type2 には int 型が格納される
using type2 = std::conditional<
    std::is_integral_v<float>,
    float,
    int
>::type;
static_assert(std::is_same_v<type2, int>);

int main() {}

また、C++14 から、エイリアステンプレート9によるstd::conditional_tの定義が追加され、テンプレートパラメータを指定するだけで結果を取得することが可能となりました。
これにより、メンバ型typeを参照する方法と比較して、より関数呼び出しに似ている書き方ができます。

#include <type_traits>

// conditional_t<...> と記述することにより、conditional<...>::type と同じ結果を取得可能
using type1 = std::conditional_t<
    std::is_integral_v<unsigned long>,
    unsigned long,
    int
>;
static_assert(std::is_same_v<type1, unsigned long>);

using type2 = std::conditional_t<
    std::is_integral_v<float>,
    float,
    int
>;
static_assert(std::is_same_v<type2, int>);

int main() {}

おわりに

C++ でのテンプレートメタプログラミングの評価はコンパイル時に行われるため、テンプレートクラスの作成者は、作成したクラスに対して意図に反した使い方ができないよう、想定外の使い方でコンパイルエラーとなるようにコーディングすることができます。
また、副作用のない処理の不具合を、コンパイル時にできる限り検出するようにしておくことで、デバッグを容易にすることも可能と考えます。

しかし、<type_traits>ヘッダに定義されているメタ関数は基本的なものが多く、複雑なメタプログラミングが必要になると、物足りなくなってくるかもしれません。
そのような場合は、C++ のライブラリである boost の mpl を導入したり、自分でメタ関数を定義する必要があります。
そのため、別の機会がありましたら、メタ関数の定義についても執筆出来たらと考えています。

以上、C++ のメタ関数について説明させていただきましたが、参考になりましたら幸いです。


  1. メタプログラミングは型操作や言語機能の制御、ソースコードの生成などの手法で、コーディングのルールや処理の共通化の方法などを記述します。
    直接敵にアプリなどの機能の実装に寄与するものではなく、ソースコードの記述量の減少や可読性の向上、デバッグをやりやすくするといったような観点から、プログラミングを支援します。
    特に、ライブラリやフレームワークのように、汎用性と拡張性が必要とされるようなソースコードで行われることが多い印象です。 ↩︎

  2. メタ関数の設計によっては、値のテンプレートパラメータを受け取る場合もあります。
    例えば、標準ライブラリで定義されているconditionalは、下記のような定義となっており、値のテンプレートパラメータBの真偽によって、TFどちらかのパラメータを返却します。

    namespace std {
        template<bool B, class T, class F>
        struct conditional;
    }
    
     ↩︎
  3. constexprconstevalのようなキーワードを指定することで、コンパイル時に評価可能な関数を定義することは可能です。
    しかし、本投稿の趣旨からは逸脱していますので、上記は考慮に含めません。 ↩︎

  4. 本投稿で紹介するもの以外にも、多くのメタ関数が<type_traits>ヘッダに定義されています。
    標準ライブラリにどのようなメタ関数が定義されているか興味のある方は、cpprefjp の type_traitsが参考になると思われます。 ↩︎

  5. より詳細なis_sameの機能について興味がある方は、cpprefjp の is_sameが参考になると思われます。 ↩︎

  6. より詳細なis_integralの機能について興味がある方は、cpprefjp の is_integralが参考になると思われます。 ↩︎

  7. より詳細なconditionalの機能について興味がある方は、cpprefjp の conditionalが参考になると思われます。 ↩︎

  8. C++14 より、変数テンプレートが言語機能として追加されました。
    上記により、関数やクラスに加え、コンパイル時定数をテンプレートとして定義可能になりました。

    詳細については cpprefjp の変数テンプレートが参考になります。 ↩︎

  9. C++11 から言語機能としてエイリアステンプレートが存在します。
    usingキーワードを用いた型の別名を宣言する言語機能として、エイリアス宣言がありますが、エイリアステンプレートでは、テンプレートを用いたエイリアス宣言を行うことができます。

    エイリアステンプレートにより宣言された型の別名は、テンプレートクラスと同様に使うことができます。

    詳細については cpprefjp のエイリアステンプレートが参考になります。 ↩︎