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

ボレロ村上 - ENiyGmaA Code

中3女子です。

リテラル型クラスの条件、および「中3女子でもわかる constexpr」の訂正

中3女子です。


これはおよそ一年半前(2011/12/3)に公開したスライドだが、この中の記述に規格上の誤りがあるので、今更だがここで訂正しておく。


問題は p.20〜『◆リテラル型クラスの条件』の項目だ。
この中の記述は、C++11 の規格策定における議論段階の文言を元にしており、これは後に文言が改訂されたため、
スライド中の記述と現行 C++11 の規格とで齟齬が生じてしまっている。


ちなみに、現行 C++11 の規格では、リテラル型クラスの条件はこのようになっている。

    • N3337 § 3.9 Types - 10 より抜粋

― a class type (Clause 9) that has all of the following properties:
 ― it has a trivial destructor,
 ― every constructor call and full-expression in the brace-or-equal-initializers for non-static data members (if any) is a constant expression (5.19),
 ― it is an aggregate type (8.5.1) or has at least one constexpr constructor or constructor template that is not a copy or move constructor, and
 ― all of its non-static data members and base classes are of literal types.

正誤表

以下に、スライド中の記述について正誤表を示す。
イタリック部分が訂正箇所である。


リテラル型クラスの条件

    • p.20

【誤】

コンパイラの要求
 - trivial コピーコンストラクタを持つ
 - 非 trivial ムーブコンストラクタを持たない
 - trivial デストラクタを持つ
 - trivial デフォルトコンストラクタ、コピーでもムーブでもない constexpr コンストラクタを持つ
 - static データメンバと基底クラスは、全てリテラル型である

【正】

 - trivial デストラクタを持つ
 - 非 static データメンバのメンバ初期化子の式全体とコンストラクタ呼出は、すべて定数式である
 - アグリゲートであるか、またはコピーでもムーブでもない constexpr コンストラクタ一つ以上持つ
 - 非 static データメンバと基底クラスは、全てリテラル型である

    • p.21

【誤】

プログラマの「べからず」
 - 仮想関数や仮想基底クラスを書かない
 - ユーザ定義コピーコンストラクタを書かない
  - (delete もしない)
 - ユーザ定義ムーブコンストラクタを書かない
  - (delete するのはよい)
 - ユーザ定義デストラクタを書かない
 - ユーザ定義デフォルトコンストラクタを書くなら原則 constexpr にする
  - (デフォルトコンストラクタが trivial でも constexpr でもない場合、別の constexpr コンストラクタを書く)
 - リテラル型以外の非 static データメンバや基底クラスを使わない

【正】

プログラマの「べからず」
 - 仮想関数や仮想基底クラスを書かない
 - ユーザ定義デストラクタを書かない
 - 非定数式のメンバ初期化子を書かない
 - ユーザ定義デフォルトコンストラクタを書くなら原則 constexpr にする
  - (デフォルトコンストラクタが trivial でも constexpr でもない場合、別の constexpr コンストラクタを書く)
 - リテラル型以外の非 static データメンバや基底クラスを使わない

解説

大きな改訂部分は、非 trivial コピー/ムーブコンストラクタを書けるようになったことである。

struct LiteralType {
    /* ユーザ定義コピーコンストラクタを持っていてよい */
    constexpr LiteralType(LiteralType const&) { }

    /* ユーザ定義ムーブコンストラクタを持っていてよい */
    constexpr LiteralType(LiteralType&&) { }
};


また、C++11 から標準化されたメンバ初期化子についての記述が追加されている。

struct LiteralType {
    /* メンバ初期化子は、定数式でなければならない */
    int t = -1; // 非 static データメンバ
};


ちなみに、trivial デフォルト/コピー/ムーブコンストラクタは暗黙で constexpr 指定される。
もちろん、defaulted された場合も同様である。

class LiteralType {
    int t;
public:
    explicit constexpr LiteralType(int t) : t(t) { }
    /* trivial コンストラクタは暗黙で constexpr である */
    LiteralType() = default;
    LiteralType(LiteralType const&) = default;
    LiteralType(LiteralType&&) = default;
};

constexpr auto t0 = LiteralType(-1);
constexpr auto t1 = LiteralType();
constexpr auto t2 = LiteralType(t1);
constexpr auto t3 = LiteralType(LiteralType());


これについて、処理系のバグも付記しておく。
非 trivial constexpr ムーブコンストラクタを持つ(または、そのような基底クラスや非 static データメンバを持つ)クラスは、規格上リテラル型である。
しかし GCC4.8.0 - 4.8.1 においてこのようなクラスを constexpr 関数の値渡しの引数に二段以上渡すと、コンパイルエラーになる。

struct Z {
    Z()         = default;
    Z(Z const&) = default;
    constexpr Z(Z&&) {} /* non-trivial (constexpr) move ctor */
};

template<typename T>
constexpr int fn0(T v) { return 0; }
template<typename T>
constexpr int fn (T v) { return fn0(v); }

constexpr auto t0 = fn0(Z()); // OK!
constexpr auto t  = fn (Z()); // error! (GCC 4.8.1, -std=c++11)

なお、Clang3.2 - 3.3 および GCC4.7 では、このコードは問題なくコンパイルされる。
これについては GCC にバグ報告をしておいた。
Bug 57901 – [4.8/4.9 Regression] Cannot call-by-value such that class has non-trivial (constexpr) move constructor