[C++]std::make_tuple、std::tie、std::forward_as_tupleの違い。

 いずれも引数を纏めてstd::tupleを作成するという意味では共通している。ただしこれら3つの振る舞いはいずれも異なっていて、状況に応じて適切に使い分ける必要がある。簡潔に言えば、std::make_tupleは引数のコピーを作り、std::tieは引数の左辺値参照を作り、std::forward_as_tupleは引数を纏めて転送する。

std::make_tuple

 基本的には引数のコピーを格納したstd::tupleを作る。与えられた引数が右辺値か左辺値かconstかなどに関わらず、その参照を取っ払ってコピーするのである。したがって引数に与えた変数と作られたstd::tupleが持つ要素は互いに独立したものになる。ただしstd::reference_wrapperを使うことで、明示的に参照として保持するよう指示することは出来る。

int x = 3;
double y = 0.14;
std::string z  ="0.001592"
auto t1 = std::make_tuple(x, y, z);//t1はstd::tuple<int, double, std::string>
auto t2 = std::make_tuple(x, std::cref(y), z);//t2はstd::tuple<int, const double&, std::string>

std::tie

 変数の型に関わらず、全て左辺値参照として保持する。つまり引数と作られたstd::tupleの間で値が共有され、std::tupleの方に値を代入すれば元の引数も変化するようになる。
 std::tupleに押し込まれた複数の戻り値を返す関数に対して、それらを分解して各々別の変数として受け取る場合に使うことが多い。のだが、私はほとんど使ったことがない。だいたいstd::getで十分であるし、C++17で構造化束縛が登場したことにより余計に立場を失ったような印象がある。構造化束縛と違い宣言済みの変数にしか代入できないので、基本的に参照型の変数に対して使えないし、デフォルトコンストラクタを持たないインスタンスに代入したい場合も厄介だ。
 ただし構造化束縛は逆に宣言済みの変数に代入することができないので、std::tieの出番がまるっきり失われたわけではない。

std::tuple<int, double, std::string> func() { return std::make_tuple(3, 0.14, "0.001592"); }
int x = 0;
double y = 0;
std::string z  ="0";
std::tie(x, y, z) = func();
//std::tieの戻り値はstd::tuple<int&, double&, std::string&>型なので、
//funcの戻り値が分解されx、y、z各変数に代入される。
//C++17ならauto[x, y, z] = func();の方が簡単。

printf("x == %d, y == %.2lf, z == %s\n", x, y, z.c_str());
//x == 3、y == 0.14, z == "0.001592"

std::forward_as_tuple

 std::forwardのように、左辺値を左辺値参照として、右辺値を右辺値参照としてstd::tupleに纏める。したがってコピーされない。
 それだけではこの関数の有意性を見いだせないかもしれないが、この関数が真価を発揮するのは複雑な可変長引数関数テンプレートを扱うときだ。std::forward_as_tupleの強みは、可変長引数テンプレートによってパックされた引数を、単一のstd::tupleに押し込みつつ完全転送できることにある。テンプレートライブラリを書いている私の場合、std::make_tupleやstd::tieよりも出番が多い。この完全転送を利用してstd::mapなどのemplace関数にも使われている。
 可変長引数テンプレートや完全転送を正しく理解していないと使いこなせないので、まずそちらを勉強すべし。

std::map<std::string, std::vector<double>> m;
m.emplace(std::piecewise_construct, std::forward_as_tuple("abcde"), std::forward_as_tuple(5, 1.0));
//emplace("abcde", 5, 1.0)と呼び出してしまうと、
//どこまでがキーのコンストラクタのための引数で、どこからが値のための引数なのかが分からない。
//よって、std::forward_as_tupleによって引数を纏めることで区別している。
//このとき、std::forward_as_tupleによって作られるstd::tupleの要素は、
//左辺値参照か右辺値参照のどちらか適切に判断されて構築される。つまり、完全転送される。

 C++を覚えて1~2年の頃は、こんな初歩的なところも理解できずに悩んだりしたものだ。ググってみても解説らしい解説がなかったので、ちょっと書き留めておく。

[C++]natvisファイルで多次元の動的配列を表示する。

 Visual Studioデバッグしている時、例えばSTLなどの中身を綺麗に整頓された状態で監視することが出来る。これを自作クラスに対しても行うことは出来るのだろうか?私は標準ライブラリでは設計上の問題が生じる場合によく自作の機能を設計して使うのだが、それらをデバッグ時に表示できないと困る。
 結論から言うと、可能だ。natvisファイルを作り、その中にデバッグ中の表示方法を定義すれば良い。基本的な使い方は「natvis」とググれば簡単に見つかるので省略する。

 例えば、自分でnew、delete(あるいはmalloc、free)するような動的配列を、Visual Studioデバッグ機能はうまく表示してくれない。普通の1次元配列はちょっと探せば解説が見つかるし難しくないのだが、多次元配列の場合は少し面倒である。ので、ここにちょっと纏めておく。

 シンプルな2次元動的配列クラスを考える。

#include <iostream>
#include <array>

struct Matrix
{
    Matrix(size_t xsize, size_t ysize) : mSize{ xsize, ysize }, mPtr(new double[xsize*ysize]) {}
    Matrix(size_t xsize, size_t ysize, double init)
        : mSize{ xsize, ysize }, mPtr(new double[xsize*ysize])
    {
        size_t size = mSize[0] * mSize[1];
        for (size_t i = 0; i < size; ++i) mPtr[i] = init;
    }
    ~Matrix() { delete[] mPtr; }
    double& operator()(size_t x, size_t y) { return mPtr[x * mSize[1] + y]; }
    const double& operator()(size_t x, size_t y) const { return mPtr[x * mSize[1] + y]; }
private:
    double* mPtr;
    size_t mSize[2];
};

int main()
{
    Matrix x(2, 3, 0);
    x(1, 2) = 3;//このあたりでデバッグを止めて中身を見てみよう。
}

 さてこのとき、mPtrの中身をきちんと2次元配列として表示したい。次のようなnatvisファイルをプロジェクトに追加しよう。

<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">

  <Type Name="Matrix">
    <DisplayString>{{ Size = {mSize[0]}, {mSize[1]} }}</DisplayString>
    <Expand>
      <ArrayItems>
        <Direction>Forward</Direction>
        <Rank>2</Rank>
        <Size>$i==0?mSize[0]:mSize[1]</Size>
        <ValuePointer>mPtr</ValuePointer>
      </ArrayItems>
    </Expand>
  </Type>
  
</AutoVisualizer>

 1次元配列と比べて定義しなければならない項目が少し増えている。
 Directionは配列の方向がどのようであるかを示している。Forwardとした場合、メモリ空間の位置と要素のindexとが[0, 0]、...、[0, nx]、[1, 0]、...、[1, nx]、...のように対応していると解釈される。Backwardなら[0, 0]、...、[nx, 0]、[0, 1]、...、[nx, 1]、...のような配置だと推定される。
 Rankは配列の次元数だ。今回は2次元配列なので、2を与えている。
 Sizeには、x方向とy方向のサイズを定義する。このとき$iという変数が自動的に与えられるのだが、これが0のときにx方向のサイズを、1のときにy方向のサイズを与える。これは3次元以降の場合も同様で、z以降も$i==2、$i==3、...のときに各値を返すよう定義すればよい。
 ValuePointerは単にポインタを与えるだけだ。1次元配列と同様である。

 ちょっとした応用例としては、OpenCVのMatなども上の定義で何とかなったと記憶している。あれも所詮は2次元(細かな色の数の指定などを考えれば3次元)の配列である。

[C++]std::anyとは異なる、動的メモリ確保を行わない動的型を作る。

更新情報

2021年3月18日
色々と設計に問題があったので全面的に作り直した。

動機

std::anyはどのような型のインスタンスでも格納でき、かつデストラクタを呼ぶ必要のない便利な機能だ。ただしインスタンスを格納するメモリは動的に確保される場合があり、しかも実装によって動的メモリ確保を回避できる最大サイズが異なっている。私が確認した限り、64bitのMSVCだと56または48バイトで、GCCのlibstdc++だとおそらく8バイト、Clangのlibc++は24バイトだと思われる。このあたりの詳しい説明はこちらの記事を参照されたい

しかし実装によって動的メモリ確保が起きたり起きなかったりするとか、最大サイズを指定できないなどの仕様は鬱陶しい。出来ることなら、例えば動的メモリ確保を起こさない最大サイズを明示的に指定できたり、動的メモリ確保が起きる大きさのインスタンスの格納は禁止したりできるとより便利だ。
もしそれが実現するのなら、例えば派生クラスのインスタンスを静的なストレージに格納しておいて、基底クラスにキャストしつつ使う、といった方法も可能だ。通常は派生クラスを基底クラスのポインタなどに格納する場合は動的メモリ確保が必要になるが、このコストを帳消しにできるのは大きい。

というわけでちょっと作ってみた。

template <size_t StrgSize, bool AllowBigObj>
class Any_impl
{
    template <class T>
    static constexpr bool IsAny()
    {
        return std::is_same_v<std::decay_t<T>, Any_impl>;
    }

    template <class T>
    static constexpr bool IsSmall()
    {
        return
            sizeof(T) <= StrgSize &&
            std::is_nothrow_move_constructible_v<T>;
    }

    struct RTFuncs
    {
        template <class T, bool Small>
        static void Copy(Any_impl& to, const Any_impl& from)
        {
            if constexpr (Small)
            {
                if constexpr (!std::is_copy_constructible_v<T>) throw std::exception("T is not copy constructible");
                else
                {
                    new (&to.mStorage.mSmall.mData) T(from.Get_unsafe<T>());
                }
            }
            else if constexpr (AllowBigObj)
            {
                if constexpr (!std::is_copy_constructible_v<T>) throw std::exception("T is not copy constructible");
                else to.mStorage.mBig.mPtr = new T(from.Get_unsafe<T>());
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T, bool Small>
        static void Move(Any_impl& to, Any_impl&& from) noexcept
        {
            if constexpr (Small)
            {
                auto& t = *std::launder(reinterpret_cast<T&>(to.mStorage.mSmall.mData));
                auto& f = *std::launder(reinterpret_cast<T&>(from.mStorage.mSmall.mData));
                new (&t) T(std::move(f));
                f.~T();
            }
            else if constexpr (AllowBigObj)
            {
                to.mStorage.mBig.mPtr = from.mStorage.mBig.mPtr;
                from.mStorage.mBig.mPtr = nullptr;
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T, bool Small>
        static void Destroy(Any_impl& to) noexcept
        {
            if constexpr (Small)
            {
                auto& t = *std::launder(reinterpret_cast<T&>(to.mStorage.mSmall.mData));
                t.~T();
                to.mStorage.mSmall = SmallStorage{};
            }
            else if constexpr (AllowBigObj)
            {
                auto* t = *std::launder(reinterpret_cast<T*>(to.mStorage.mBig.mPtr));
                delete t;
                to.mStorage.mBig = BigStorage{};
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T>
        static const std::type_info& TypeInfo()
        {
            return typeid(T);
        }
        using CopyF = void(*)(Any_impl&, const Any_impl&);
        using MoveF = void(*)(Any_impl&, Any_impl&&);
        using DestroyF = void(*)(Any_impl&);
        using TypeInfoF = const std::type_info& (*)(void);

        CopyF mCopy = nullptr;
        MoveF mMove = nullptr;
        DestroyF mDestroy = nullptr;
        TypeInfoF mTypeInfo = nullptr;
    };
    template <class T, bool Small = IsSmall<T>()>
    inline static constexpr RTFuncs RTFuncs_value =
    { &RTFuncs::template Copy<T, Small>, &RTFuncs::template Move<T, Small>,
      &RTFuncs::template Destroy<T, Small>, &RTFuncs::template TypeInfo<T> };

public:

    struct NullType {};

    Any_impl() : mRTFuncs(nullptr) {}
    template <class T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, Any_impl>, std::nullptr_t> = nullptr>
    Any_impl(T&& v)
        : mRTFuncs(nullptr)
    {
        Emplace_impl<std::decay_t<T>>(std::forward<T>(v));
    }
    Any_impl(const Any_impl& a)
        : mRTFuncs(nullptr)
    {
        if (!a.IsEmpty()) Copy(a);
    }
    Any_impl(Any_impl&& a) noexcept
        : mRTFuncs(nullptr)
    {
        if (!a.IsEmpty()) Move(std::move(a));
    }

    template <class T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, Any_impl>, std::nullptr_t> = nullptr>
    Any_impl& operator=(T&& v)
    {
        Emplace<std::decay_t<T>>(std::forward<T>(v));
        return *this;
    }

    Any_impl& operator=(const Any_impl& a)
    {
        if (!IsEmpty()) Destroy();
        if (!a.IsEmpty()) Copy(a);
        return *this;
    }
    Any_impl& operator=(Any_impl&& a) noexcept
    {
        if (!IsEmpty()) Destroy();
        if (!a.IsEmpty()) Move(std::move(a));
        return *this;
    }
    ~Any_impl()
    {
        if (!IsEmpty()) Destroy();
    }

    template <class T, class ...Args>
    void Emplace(Args&& ...args)
    {
        if (!IsEmpty()) Destroy();
        Emplace_impl<T>(std::forward<Args>(args)...);
    }

    bool IsEmpty() const { return mRTFuncs == nullptr; }
    operator bool() const { return !IsEmpty(); }
    template <class T>
    bool Is() const
    {
        return !IsEmpty() && typeid(T) == TypeInfo();
    }
    const std::type_info& GetType() const
    {
        if (IsEmpty()) return typeid(NullType);
        return TypeInfo();
    }

    template <class T>
    const T& Get() const&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }
    template <class T>
    T& Get()&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }
    template <class T>
    T&& Get()&&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }

    template <class T>
    const T& Get_unsafe() const&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return *std::launder(reinterpret_cast<const T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return *std::launder(reinterpret_cast<const T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }
    template <class T>
    T& Get_unsafe()&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return *std::launder(reinterpret_cast<T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return *std::launder(reinterpret_cast<T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }
    template <class T>
    T&& Get_unsafe()&&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return std::move(*reinterpret_cast<T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return std::move(*reinterpret_cast<T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }

    template <class T, bool B = AllowBigObj, std::enable_if_t<!B, std::nullptr_t> = nullptr>
    T& Cast()&
    {
        return *std::launder(reinterpret_cast<T&>(mStorage.mSmall.mData));
    }
    template <class T, bool B = AllowBigObj, std::enable_if_t<!B, std::nullptr_t> = nullptr>
    const T& Cast() const&
    {
        return *std::launder(reinterpret_cast<const T&>(mStorage.mSmall.mData));
    }

private:

    void Copy(const Any_impl& from)
    {
        assert(IsEmpty());
        mRTFuncs = from.mRTFuncs;
        mRTFuncs->mCopy(*this, from);
    }
    void Move(Any_impl&& from) noexcept
    {
        assert(IsEmpty());
        mRTFuncs = from.mRTFuncs;
        mRTFuncs->mMove(*this, std::move(from));
        from.mRTFuncs = nullptr;
    }
    void Destroy() noexcept
    {
        assert(!IsEmpty());
        auto* d = mRTFuncs->mDestroy;
        mRTFuncs = nullptr;
        d(*this);
    }
    const std::type_info& TypeInfo() const
    {
        assert(!IsEmpty());
        return mRTFuncs->mTypeInfo();
    }

    //Tはdecayされているものとする。
    template <class T, class ...Args>
    void Emplace_impl(Args&& ...args)
    {
        assert(IsEmpty());
        if constexpr (IsSmall<T>())
        {
            //small
            new (&mStorage.mSmall.mData) T(std::forward<Args>(args)...);
            mRTFuncs = &RTFuncs_value<T>;
        }
        else if constexpr (AllowBigObj)
        {
            //big
            mStorage.mBig.mPtr = new T(std::forward<Args>(args)...);
            mRTFuncs = &RTFuncs_value<T>;
        }
        else
            static_assert([]() { return false; }, "size of the template argument is greater than Any's storage.");
    }

    struct BigStorage
    {
        char Padding[StrgSize - sizeof(void*)];
        void* mPtr;
    };
    struct SmallStorage
    {
        std::aligned_storage_t<StrgSize, alignof(std::max_align_t)> mData;
    };
    template <bool, class = void>
    union Storage
    {
        BigStorage mBig;
        SmallStorage mSmall;
    };
    template <class Dummy>
    union Storage<false, Dummy>
    {
        SmallStorage mSmall;
    };

    Storage<AllowBigObj> mStorage;
    const RTFuncs* mRTFuncs;
};

//Any_implのテンプレート引数にはストレージの大きさ、動的メモリ確保を許すか否かを与える。

//Any_impl<Size, true>とした場合、
// * 代入するオブジェクトがSizeバイト以下かつnothrow move constructibleならAny_impl内のストレージにplacement newで構築される。
// * Sizeバイトより大きいなら、動的メモリ確保を行いそこにオブジェクトを格納する。Any_implはそこへのポインタを保有する。
using Any = Any_impl<24, true>;

//Any_impl<Size, false>とした場合、
// * 代入するオブジェクトがSizeバイト以下かつnothrow move constructibleならAny_impl<Size, true>に同じ。
// * Sizeバイトより大きいオブジェクトは禁止され、格納しようとするとコンパイルエラーになる。
template <size_t Size>
using StaticAny = Any_impl<Size, false>;

ここで定義しているAny_impl<Size, AllowBigObj>は、Size + sizeof(void*)の大きさを持ち、Sizeバイトまでなら動的メモリ確保なしに格納することが出来る(ただしnothrow move constructibleでなければならない)。AllowBigObjをtrueにした場合、Sizeバイトを超えるオブジェクトも動的メモリ確保によって格納することが出来るが、falseを与えた場合はそれも禁止される。

この機能を作った目的の一つは派生クラスの動的メモリ確保を回避することだった。AllowBigObj == falseとした場合に限り、これを可能にするCast関数を用意した。trueの場合は原理的に実現不可能である。

struct Base
{
    virtual void Func() const = 0;
};
struct Derived1 : public Base { virtual void Func() const { std::cout << "Derived1" << std::endl; } };
struct Derived2 : public Base { virtual void Func() const { std::cout << "Derived2" << std::endl; } };

int main()
{
    //32バイト以下のオブジェクトを格納できる。
    StaticAny<32> a = std::vector<double>{ 1, 2, 3, 4, 5 };
    assert(a.Is<std::vector<double>>());
    for (auto d : a.Get<std::vector<double>>()) std::cout << d;
    std::cout << std::endl;
    //33バイト以上のオブジェクトは格納できない。
    //StaticAny<32> b = std::array<double, 5>{ 1, 2, 3, 4, 5 };コンパイルエラー。

    //25バイト以上のオブジェクトは動的メモリ確保を伴って格納される。
    Any b = std::string("abcde");
    assert(b.Is<std::string>());
    std::cout << b.Get<std::string>() << std::endl;

    //std::anyはnon-copyableなオブジェクトを格納できないが、Any_implは可能。
    //コピーしようとすると例外を投げる。
    StaticAny<8> c = std::make_unique<int>(10);
    std::cout << *c.Get<std::unique_ptr<int>>() << std::endl;
    try
    {
        StaticAny<8> d = c;
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }

    //AllowBigObj == falseの場合は、格納されている派生クラスのオブジェクトを基底クラスにキャストする、なんて使い方もできる。
    //動的メモリ確保を行うことなく派生クラスを扱うことも可能。
    //Baseクラスの型情報では格納先がAny_impl内ストレージか動的確保されたメモリかがわからないため、AllowBigObj == trueの場合はこの使い方は許されない。
    StaticAny<8> any;
    any = Derived1();
    any.Cast<Base>().Func();//Castは型チェックなど諸々省くので非常に危険だが、仮想基底クラスなどへもキャストできる。
    any = Derived2();
    any.Cast<Base>().Func();
    return 0;
}

 この程度の機能は誰かがすでに作っているかもしれないと思って探してみたら、案の定GitHubで見つかった。……しかも中身を見てみると基本設計が私のものとほぼ同じだった。わざわざ作らなくても良かったなぁ。いや、大した手間でもなかったし、名前付けの法則などが私と異なっていて気持ち悪いので、自前で作るに越したことはないのだが。