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

ボレロ村上 - ENiyGmaA Code

中3女子です。

C++11の糞仕様と戦ってアクセッサをconstexprにする

中3女子です。


C++11が、C++03およびそれ以前に対して明らかに優れていることは今更言うまでもない。
しかしながら、C++11にも恥ずべき糞仕様は存在する。
その糞の代表例が、constexprメンバ関数が暗黙でconst修飾されることである。
これによってどんな問題が生ずるかを、まずは見てゆく。

constexprメンバ関数が暗黙でconst修飾される糞仕様の問題

まずは、適当な値のホルダクラスを考える。

template<typename T>
struct Holder {
    T value;
};


Holderはリテラルクラスの要件を満たすから、当然定数式として扱える。

constexpr auto t = Holder<int>{ 100 };
constexpr int i = t.value;


では、下記のコードは合法か?

constexpr int i = Holder<int>{ 100 }.value; // データメンバを参照

勿論イエス。
prvalue(一時オブジェクト)やrvalue参照からのデータメンバの参照は、定数式になることが出来る。


ところで、std::optionalのようなクラスや、あるいはデータメンバを隠蔽したい場合は通常、アクセッサメンバ関数を書く。
setter/getterを別々に書く場合もあるが、optional::value()のように、保持するデータへの参照を返したほうがよい場合も多い。


よく知られているように、メンバ関数は*thisに対するconst修飾の有無によってオーバーロードすることができる。
つまり、以下のようになる。(全く隠蔽になってないのは、単純化のためなので無視戴きたい)

template<typename T>
struct Holder2 {
    T v_;
    constexpr T&       value()       { return v_; } /* 非const版 */
    constexpr T const& value() const { return v_; } /* const版 */
};

しかしながら、このコードはC++11においてill-formedである。


なぜならば、constexprメンバ関数が暗黙でconst修飾されるから、上記のコードは下記と同じに解釈される。

template<typename T>
struct Holder2 {
    T v_;
    constexpr T&       value() const { return v_; } /* Oops! 非const版……にならない */
    constexpr T const& value() const { return v_; } /* const版 */
};

結果的に、返値型のみが異なるメンバ関数の定義であると見做され、コンパイルエラーになる。


C++11において、非const版とconst版のアクセッサメンバ関数を書くには、最も安易な方法は非const版をconstexpr指定しないことである。

template<typename T>
struct Holder2 {
    T v_;
              T&       value()       { return v_; } /* 非const版 */
    constexpr T const& value() const { return v_; } /* const版 */
};


これでめでたくwell-formedなクラスとなり、定数式としても扱える。

constexpr auto t = Holder2<int>{ 100 };
constexpr int i = t.value();


ところが、今度は別な問題が生ずる。それはprvalueやrvalue参照のオブジェクトからアクセッサを呼ぶ場合である。

constexpr int i = Holder2<int>{ 100 }.value(); // Oops! 非const版が呼ばれる

Holder2{ 100 }は、非constなrvalueである。
constなprvalueやrvalue参照からのメンバ関数呼び出しは、非const版が優先される。
そしてHolder2::value()の非const版はconstexprではない。
定数式が必要な文脈でのnon-constexpr関数の呼出はill-formedであるから、無残にもコンパイルエラーになる。


constexprという予約語の字面のせいで時々誤解が起こるが、const性が担保されるのは、評価が完了したコンパイル時定数のみである。
定数式評価中の文脈におけるオブジェクトのconst性とはまったく無縁である。
たとえコンパイル時であっても、非const rvalue/lvalue参照などの、あらゆるvalue categoryの式が扱われる。


constexpr変数が暗黙でconst修飾される(しかも constexpr const T と書くことが出来ない)のは、個人的に好みではないが、まだ妥当性のある仕様といえる。
コンパイル時定数は当然constであるのが自明だからだ。
しかしながら、constexprメンバ関数が暗黙でconst修飾されるのは論外である。
定数式の評価結果に対するconst性と、定数式評価中のconst性を取り違えた、致命的な失策の、腐敗した糞仕様と言わざるをえない。

アクセッサをconstexprにするには

糞仕様もまた仕様なり。
C++11がC++11である限り、その中で実装しなければならない。


ここで問題となっているのは、非staticメンバ関数である。
ならば、staticメンバ関数やフリー関数を使えばよい。

template<typename T>
struct Holder3 {
    T v_;
    static constexpr T&       value(Holder3& t)       { return t.v_; }                       /* 非const lvalue参照版 */
    static constexpr T&&      value(Holder3&& t)      { return static_cast<T&&>(value(t)); } /* 非const rvalue参照版 */
    static constexpr T const& value(Holder3 const& t) { return t.v_; }                       /* const版 */
};

staticメンバ関数を使えば、lvalue参照版とrvalue参照版を分けることも容易である。
(rvalue参照はlvalueなので、static_castでT&&にforwardしている点に注意されたし)
ところで、このように自己のstaticメンバ関数を使ってデータの参照を得させる手法は、libstdc++などの典型的なstd::tupleの内部実装にも用いられている。

using H = Holder3<int>;
constexpr int i = H::value(H{ 100 });

とはいえ、内部実装ならともかくユーザコードで使うにはこのように、いかにも冗長になってしまう。

template<typename T>
constexpr T&       value(Holder3<T>& t)       { return Holder3<T>::value(t); }       /* 非const lvalue参照版 */
template<typename T>
constexpr T&&      value(Holder3<T>&& t)      { return static_cast<T&&>(value(t)); } /* 非const rvalue参照版 */
template<typename T>
constexpr T const& value(Holder3<T> const& t) { return Holder3<T>::value(t); }       /* const版 */

このようなフリー関数を追加すれば、

constexpr int i = value(Holder3<int>{ 100 });

このようにすっきり書くことができる。


C++erの間では昔から言われている、メンバ関数を増やすよりもフリー関数を使うべき」という言説の根拠がこれでまた一つ増えた。

C++14

C++14では、constexprメンバ関数が暗黙でconst修飾されるという、C++11最悪の失敗であるこの忌まわしい糞仕様が削除される。
N3652
つまり、最初に書いたようなコードがそのままコンパイル出来る(はずである)。

template<typename T>
struct Holder4 {
    T v_;
    constexpr T&       value()       { return v_; }                   /* 非const lvalue参照版 */
    constexpr T&&      value() &&    { return static_cast<T&&>(v_); } /* 非const rvalue参照版 */
    constexpr T const& value() const { return v_; }                   /* const版 */
};

素晴らしい。


ともあれ、フリー関数が様々な点で優れていることには変わりないので、両方の選択肢があるならば、互換性も含めてフリー関数の方をより使うべきだと個人的には思う。


ところで、C++14のstd::optionalやstd::arrayなどのメンバ関数は、上記の仕様改善にも係わらず、非const版はconstexpr指定されていない。
これは、標準ライブラリはC++14においても、未だC++11時代のconstexprの仕様を念頭に設計されているということを示している。
標準ライブラリの仕様は、よほど先進的な部分を除いてコア言語に追従して改訂されることになる以上、仕方のないことだとも言える。


幸いなことに、C++11からC++14への大幅改修を見ても分かるように、constexprは標準化委員会においても大分スポットが当たっているように思える。
更に次期規格C++1yでの改善を大いに期待したい。
なお、C++14におけるconstexprについては、Boost.勉強会 #12で詳細に解説したいと思う。