9
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?

C++ TMP (テンプレートメタプログラミング) シリーズ

Part1 constexpr Part2 Concepts Part3 Variadic Part4 型リスト Part5 正規表現
👈 Now

はじめに

C++のテンプレートは強力だけど、エラーメッセージが地獄だった。

error: no matching function for call to 'sort'
note: candidate template ignored: substitution failure [with T = MyClass]:
      no member named 'operator<' in 'MyClass'
      ...(数十行のエラーが続く)

C++20のコンセプトでこれが劇的に改善された。

コンセプトとは

型の要件を宣言的に記述する機能。

// コンセプトの定義
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

// コンセプトを使った関数
template<Addable T>
T add(T a, T b) {
    return a + b;
}

add(1, 2);      // OK
add(1.5, 2.5);  // OK

struct NoAdd {};
add(NoAdd{}, NoAdd{});  // 明確なエラー: "NoAdd does not satisfy Addable"

4つの構文

// 1. requires句
template<typename T>
    requires Addable<T>
T add1(T a, T b) { return a + b; }

// 2. テンプレートパラメータ
template<Addable T>
T add2(T a, T b) { return a + b; }

// 3. 省略形(関数パラメータ)
auto add3(Addable auto a, Addable auto b) { 
    return a + b; 
}

// 4. 後置requires句
template<typename T>
T add4(T a, T b) requires Addable<T> {
    return a + b;
}

どれを使ってもOK。好みで選ぶ。

標準コンセプト

<concepts>ヘッダに多数定義されている。

#include <concepts>

// 基本コンセプト
template<std::integral T>              // 整数型
void process_int(T value);

template<std::floating_point T>        // 浮動小数点型
void process_float(T value);

template<std::signed_integral T>       // 符号付き整数
void process_signed(T value);

template<std::unsigned_integral T>     // 符号なし整数
void process_unsigned(T value);

主な標準コンセプト

コンセプト 意味
same_as<T, U> TとUが同じ型
derived_from<D, B> DがBの派生型
convertible_to<From, To> FromからToに変換可能
integral 整数型
floating_point 浮動小数点型
copyable コピー可能
movable ムーブ可能
regular 正則型(デフォルト構築、コピー、等価比較可能)

requires式

コンセプトの本体で使う要件の記述方法

単純要件

template<typename T>
concept HasSize = requires(T t) {
    t.size();  // この式が有効であること
};

型要件

template<typename T>
concept HasValueType = requires {
    typename T::value_type;  // この型が存在すること
};

複合要件

template<typename T>
concept Indexable = requires(T t, size_t i) {
    // 式が有効で、結果の型がT::value_typeへ変換可能
    { t[i] } -> std::convertible_to<typename T::value_type>;
};

ネストされた要件

template<typename T>
concept Container = requires(T t) {
    typename T::value_type;
    typename T::iterator;
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.end() } -> std::same_as<typename T::iterator>;
    { t.size() } -> std::convertible_to<size_t>;
    requires std::copyable<T>;  // 追加の要件
};

コンセプトの合成

論理演算子

// AND: &&
template<typename T>
concept SignedNumber = std::integral<T> && std::signed_integral<T>;

// OR: ||
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;

// NOT: !
template<typename T>
concept NotPointer = !std::is_pointer_v<T>;

実践例

template<typename T>
concept Arithmetic = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
    { a - b } -> std::same_as<T>;
    { a * b } -> std::same_as<T>;
    { a / b } -> std::same_as<T>;
} && std::regular<T>;

template<Arithmetic T>
class Vector3 {
    T x, y, z;
public:
    constexpr Vector3(T x, T y, T z) : x(x), y(y), z(z) {}
    
    constexpr Vector3 operator+(const Vector3& other) const {
        return {x + other.x, y + other.y, z + other.z};
    }
    
    constexpr T dot(const Vector3& other) const {
        return x * other.x + y * other.y + z * other.z;
    }
};

オーバーロード解決

コンセプトでより制約の強いオーバーロードが優先される。

template<typename T>
void print(T value) {
    std::cout << "generic: " << value << "\n";
}

template<std::integral T>
void print(T value) {
    std::cout << "integral: " << value << "\n";
}

template<std::signed_integral T>
void print(T value) {
    std::cout << "signed integral: " << value << "\n";
}

print(3.14);   // generic: 3.14
print(42u);    // integral: 42
print(-10);    // signed integral: -10

サブセット関係が自動的に認識される。

generic < integral < signed_integral
                   < unsigned_integral
        < floating_point

実践例1: イテレータコンセプト

template<typename I>
concept Iterator = requires(I i) {
    typename std::iter_value_t<I>;
    { *i } -> std::same_as<std::iter_reference_t<I>>;
    { ++i } -> std::same_as<I&>;
    { i++ };
};

template<typename I>
concept ForwardIterator = Iterator<I> && 
    std::copyable<I> &&
    requires(I i) {
        { i == i } -> std::convertible_to<bool>;
    };

template<typename I>
concept RandomAccessIterator = ForwardIterator<I> &&
    requires(I i, I j, std::iter_difference_t<I> n) {
        { i + n } -> std::same_as<I>;
        { i - n } -> std::same_as<I>;
        { i - j } -> std::same_as<std::iter_difference_t<I>>;
        { i[n] } -> std::same_as<std::iter_reference_t<I>>;
        { i < j } -> std::convertible_to<bool>;
    };

実践例2: Rangeコンセプト

template<typename R>
concept Range = requires(R r) {
    { std::ranges::begin(r) } -> Iterator;
    { std::ranges::end(r) };
};

template<typename R>
concept SizedRange = Range<R> && requires(R r) {
    { std::ranges::size(r) } -> std::convertible_to<size_t>;
};

template<SizedRange R>
void process_sized(R&& range) {
    auto size = std::ranges::size(range);
    std::cout << "Size: " << size << "\n";
}

process_sized(std::vector{1, 2, 3});
process_sized(std::array{1, 2, 3, 4, 5});

実践例3: シリアライザブル

template<typename T>
concept Serializable = requires(T t, std::ostream& os, std::istream& is) {
    { t.serialize(os) } -> std::same_as<void>;
    { T::deserialize(is) } -> std::same_as<T>;
};

template<typename T>
concept HasToString = requires(T t) {
    { t.to_string() } -> std::convertible_to<std::string>;
};

// 両方を満たす型用の特殊化
template<typename T>
    requires Serializable<T> && HasToString<T>
void save_with_log(T& obj, std::ostream& os) {
    std::cout << "Saving: " << obj.to_string() << "\n";
    obj.serialize(os);
}

実践例4: 数学演算

template<typename T>
concept Numeric = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
    { a - b } -> std::convertible_to<T>;
    { a * b } -> std::convertible_to<T>;
    { a / b } -> std::convertible_to<T>;
    { -a } -> std::convertible_to<T>;
    { a == b } -> std::convertible_to<bool>;
    { a < b } -> std::convertible_to<bool>;
};

template<typename T>
concept NumericWithSqrt = Numeric<T> && requires(T a) {
    { std::sqrt(a) } -> std::convertible_to<T>;
};

template<NumericWithSqrt T>
T distance(T x1, T y1, T x2, T y2) {
    T dx = x2 - x1;
    T dy = y2 - y1;
    return std::sqrt(dx * dx + dy * dy);
}

SFINAEとの比較

Before: SFINAE (C++17以前)

// 読みにくい...
template<typename T>
typename std::enable_if<
    std::is_integral_v<T> && std::is_signed_v<T>,
    T
>::type
abs_sfinae(T value) {
    return value < 0 ? -value : value;
}

// もっと読みにくい...
template<typename T, 
         typename = std::enable_if_t<std::is_integral_v<T>>>
T double_sfinae(T value) {
    return value * 2;
}

After: コンセプト (C++20)

// すっきり!
template<std::signed_integral T>
T abs_concept(T value) {
    return value < 0 ? -value : value;
}

template<std::integral T>
T double_concept(T value) {
    return value * 2;
}

エラーメッセージの改善

Before: SFINAE

error: no matching function for call to 'process'
note: candidate template ignored: substitution failure 
      [with T = MyClass]: no type named 'type' in 
      'std::enable_if<false, void>'

After: コンセプト

error: constraints not satisfied for function template 'process'
note: because 'MyClass' does not satisfy 'Hashable'
note: because 'std::hash<MyClass>{}(val)' would be invalid

何が足りないのか一目瞭然

高度なテクニック

条件付きコンセプト

template<typename T, typename U = T>
concept Addable = requires(T t, U u) {
    { t + u };
};

// 同じ型同士
static_assert(Addable<int>);

// 異なる型
static_assert(Addable<int, double>);
static_assert(!Addable<std::string, int>);

コンセプトのエイリアス

template<typename T>
concept InputRange = std::ranges::input_range<T>;

template<typename T>
concept OutputRange = std::ranges::output_range<T, std::ranges::range_value_t<T>>;

template<InputRange R>
auto first(R&& range) {
    return *std::ranges::begin(range);
}

まとめ

機能 メリット
宣言的な型制約 コードの意図が明確
エラーメッセージ改善 デバッグが楽
オーバーロード解決 自然な特殊化
合成可能 再利用性

コンセプトはテンプレートの民主化。これまでTMPは上級者向けだったけど、コンセプトで敷居が下がった。

次回はバリアディックテンプレートの魔術を解説する。

続きが気になったら、いいね・ストックしてもらえると嬉しいです!

9
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
9
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?