structured bindings(構造化束縛)を自作クラスで行えるようにする

はじめに

structured bindings (構造化束縛) をstd::pair<>std::tuple<>以外の自作のクラスに対して適用するための方法をコード例込みで解説している日本語の記事が見つからなかったのでメモを兼ねて残しておきます。

結論

  1. std::tuple_size<T>を特殊化する

  2. std::tuple_element<N, T>を特殊化する

  3. get<N>(T)/T::get<N>()を定義する

structured bindings (構造化束縛)

Structured binding declaration (since C++17) - cppreference.com

構造化束縛 - cpprefjp C++日本語リファレンス

structured bindings は C++17 に追加された有用な機能で、関数の返り値からの多値の受け取りなどを簡便に行える言語機能です。

* 関数から複数の値を受け取る例

// 複数の値を返す関数
std::tuple<int, std::string, double> f() {
    return std::make_tuple(42, "foo", 3.14);
}

int main() {
    // 関数からの返り値を受ける
    auto [n, s, d] = f();
    
    assert(n == 42);
    assert(s == "foo");
    assert(d == 3.14);
}

上の例を structured bindings を使わずに書くと例えば次のようになります。

* structured bindings を使わずに関数から複数の値を受け取る例

int main() {
    int n;
    std::string s;
    double d;

    std::tie(n, s, d) = f();
    
    assert(n == 42);
    assert(s == "foo");
    assert(d == 3.14);
}

structured bindings は、std::tuple<>以外にもいくつかの型についてその恩恵を受けることが出来ます。

* std::tuple<>以外の多値型を受け取る例

{
    // std::pair<>
    auto [a, b] = std::make_pair("bar", 100);
}
    
{
    // 生配列
    int raw_array[3] = {1, 2, 3};
    
    auto [a, b, c] = raw_array;
}
    
{
    // std::array<>
    std::array<int, 3> array = {1, 2, 3};
    
    auto [a, b, c] = array;
}

{
    // 構造体
    // 非staticなメンバ変数はすべてpublicである必要がある
    struct S {
        std::string a;
        int b;
    };
    
    auto [a, b] = S { "bar", 10 };
}

自作クラスに structured bindings を適用する

上記に当てはまらないようなクラスについても structured bindings を適用することが出来ます。

例えば次のような privateな 非staticメンバ変数を持つクラスColorを考えます。

* 自作クラス

class Color {
    std::uint32_t c;

public:
    Color(std::uint32_t c) : c(c) {}

    std::uint8_t red() const { return c >> 16; }
    std::uint8_t green() const { return c >> 8; }
    std::uint8_t blue() const { return c >> 0; }
};

(Colorは内部に色の16進表現を保存し、メンバ関数red(), green(), blue() でRGBそれぞれの色要素を取り出せるようなクラスを想定しています)

これをauto [r, g, b] = ...のように受け取りたいのですが、そのままでは当然エラーになってしまいます。

int main() {
    Color orange = 0xff8000;

    auto [r, g, b] = orange; // エラー: cannot decompose non-public member 'Color::c' of 'Color'
}

一度std::tuple<>に変換する関数を用意してauto [r, g, b] = orange.as_tuple();のように書いてもいいのですが直截的でないので別の方法をとります。

1. std::tuple_size<>を特殊化する

まずはじめに、structured bindings で受け取る変数の個数をコンパイラに示します。 コンパイラstd::tuple_size<>::valueの値でこれを確認するため、std::tuple_size<Color>を特殊化します。

* std::tuple_size<>の特殊化

namespace std {
    template <>
    struct tuple_size<Color> : integral_constant<size_t, 3> {}; // Color は 3要素である
}

2. std::tuple_element<>を特殊化する

次に要素の型をコンパイラに示します。 これにはstd::tuple_element<>::typeが使われるため、std::tuple_element<N, Color>を特殊化します。

* std::tuple_element<>の特殊化

namespace std {
    template <size_t N>
    struct tuple_element<N, Color> {
        using type = uint8_t; // 要素の型はすべて std::uint8_t
    };
}

3-a. フリー関数get<>()を定義する

最後に実際に要素の値を取り出します。 これには関数get<N>(Color)が用いられるのでそれを定義します。

使用される関数はADLによって探索されるので目的のクラスと同じ名前空間に定義すれば良いでしょう。

* get<>()の定義

template <std::size_t N>
uint8_t get(const Color& c) {
    if constexpr (N == 0)
        return c.red();
    else if constexpr (N == 1)
        return c.green();
    else
        return c.blue();
}

以上で自作クラスColorに structured bindings を適用することが可能になります。

* Colorに対する structured bindings

int main() {
    Color orange = 0xff8000;
    
    auto [r, g, b] = orange;
    
    assert(r == 255);
    assert(g == 128);
    assert(b ==   0);
}

3-b. メンバ関数get<>()を定義する

フリー関数get<>()の代わりにメンバ関数get<>()を使用することも可能です。

* メンバ関数Color::get<>()の定義

class Color {
    ...

    template <std::size_t N>
    std::uint8_t get() const {
        if constexpr (N == 0)
            return red();
        else if constexpr (N == 1)
            return green();
        else
            return blue();
    }
};

フリー関数get<N>(Color)と、メンバ関数Color::get<N>()が同時に定義されていた場合、メンバ関数Color::get<N>()が優先的に使用されます。

まとめ

structured bindings はGCC/ClangのみでなくMSVCでも利用することの出来る優れた機能です。

C++は複数の値を関数から返すことのできない言語だ」などと揶揄されることもしばしばありましたが今やその限りではありません。

* 全コード

#include <cassert>
#include <tuple>
#include <iostream>

class Color {
    std::uint32_t c;

public:
    Color(std::uint32_t c) : c(c) {}

    std::uint8_t red() const { return c >> 16; }
    std::uint8_t green() const { return c >> 8; }
    std::uint8_t blue() const { return c >> 0; }
    
    template <std::size_t N>
    std::uint8_t get() const {
        std::cout << "Color::get<" << N << ">()" << std::endl;
    
        if constexpr (N == 0)
            return red();
        else if constexpr (N == 1)
            return green();
        else
            return blue();
    }
};

template <std::size_t N>
uint8_t get(const Color& c) {
    std::cout << "get<" << N << ">(Color)" << std::endl;
    
    if constexpr (N == 0)
        return c.red();
    else if constexpr (N == 1)
        return c.green();
    else
        return c.blue();
}

namespace std {
    template <>
    struct tuple_size<Color> : integral_constant<size_t, 3> {}; // Color は 3要素である
    
    template <size_t N>
    struct tuple_element<N, Color> {
        using type = uint8_t; // 要素の型はすべて std::uint8_t
    };
}

int main() {
    Color orange = 0xff8000;
    
    // こう受け取りたい
    auto [r, g, b] = orange;
    
    assert(r == 255);
    assert(g == 128);
    assert(b ==   0);
}