読者です 読者をやめる 読者になる 読者になる

とくにあぶなくないRiSKのブログ

危ないRiSKのブログだったかもしれない。本当はRiSKだけどググラビリティとか取得できるIDの都合でsscriskも使ったり。

constexprとrvalue参照問題

C++

constexprとメンバ関数の呼び分け問題

小ネタ - constexpr の文脈でconstメンバ関数と非constメンバ関数を呼び分ける - ボレロ村上 - ENiyGmaA Code

このエントリで指摘されているように、constexprとメンバ関数の呼び分け問題があります。一時オブジェクトはconst修飾されていないために、constexprであるconst修飾メンバ関数が呼ばれないのが原因です。

template<typename T>
struct X {
    T t;
    T& get() { /* 非const版 */
        return t;
    }
    constexpr T const& get() const { /* const版 */
        return t;
    }
};

template<typename T>
constexpr auto get(T&& t) -> decltype(t.get()) { /* 引数を T&& で受ける */
    return t.get();
}

int main() {
    constexpr int i = get( X<int>{1} );
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ エラーになっていますね。

メンバ関数get内のreturn t.get();に注目すると、tX&&型である(const修飾されていない)ために、constexpr T const& get() constではなくT& get()が呼ばれてしまい、結果constexprではなくなってしまいます。

Xconst_castを使ってconst修飾することで問題を解決しているようです。

constexprとmove阻害問題

moveを阻害しないconstexprの文脈でのconstメンバ関数と非constメンバ関数の呼び分け - ここは匣

さらにこのエントリで指摘されているように、「constexprとメンバ関数の呼び分け問題」に対処しようとすると、constexprとmove阻害問題が起きます。

const修飾をして、無理やりconstexpr版のメンバ関数を呼ぶので、メンバ関数の返却値の型までconst修飾されてしまい、正しくmoveされなくなってしまいます。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

そこで、X<T>const_castを使ってconst修飾した後、さらにTconst_castを使ってconstはがしして問題を解決しているようです。

constexprとrvalue参照問題

「constexprとメンバ関数の呼び分け問題」と「constexprとmove阻害問題」は、constexprとrvalue参照問題と言い換えることができます。rvalue参照は一時オブジェクトも含みます。

そもそも「constexpr の文脈」とは何でしょうか? それはT const &T&&のケースです。ですから、それを素直に表せばいいのではないでしょうか。今のC++(C++11,C++14)には、その機能があります。

それはメンバ関数の参照修飾(ref-qualifier)です *1

// clang 3.2, 3.3に対応する場合はsproutを利用する

#include<utility>
// #include<sprout/utility/move.hpp>
// #include<sprout/utility/forward.hpp>

template<class T>
struct X
{
    T t;
    T& get() & // ref-qualifierを使う場合、すべてのオーバーロードにref-qualifierが必要
    {
        return t;
    }
    constexpr T const& get() const & // ココ!
    {
        return t;
    }
    constexpr T get() && // ココ!
    {
        return std::move(t);
        // return sprout::move(t);
    }
};

template<class T>
constexpr auto get(T&& t) -> decltype(std::forward<T>(t).get())
{
    return std::forward<T>(t).get(); // シンプル!
    // return sprout::forward<T>(t).get();
}


#include <iostream>

template<class T>
struct Y
{
    Y(const T&) { std::cout << "copy" << std::endl; }
    Y(T&&) { std::cout << "move" << std::endl; }
};

int main()
{
    static_assert(std::is_same<decltype(get(X<int>{0})), int>::value, "");
    Y<int> y((get(X<int>{1}))); // move対応! ヤター!
    constexpr int a = get(X<int>{2}); // constexpr対応! ヤター!
    std::cout << a << '\n';
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

このように、クラス側でT const &T&&オーバーロードconstexpr指定すると、非メンバ関数getでは、ごく普通にstd::forward<T>(t).get()するだけで、constexpr対応かつmove対応できます。

ただし、このコードが動作するのは現状Clangだけです。GCCでは現状最新の4.10 HEADを含むすべてで動作しません。GCCでは

prog.cc:17:17: note: candidate: constexpr T X<T>::get() const && [with T = int]

のようにエラーが出てしまいます。これはconstexpr指定でメンバ関数が勝手にconst修飾されるC++11の仕様のためで、GCCはまだC++14のconstexprに対応していないようです。

このエントリはC++14以降に対応したコンパイラのみで有効です。なおメンバ関数の参照修飾自体はC++11からあります。

*1:当然、それに加えcv修飾(cv-qualifier)も含みます