yohhoyの日記

2017-09-08

シーケンスコンテナ×イテレータ・ペア×リスト初期化×推論ガイド=?

C++1z(C++17)標準ライブラリのシーケンスコンテナ(std::vectorなど)にはクラステンプレート引数推論 推論ガイド(deduction guide) が追加されたが、イテレータ・ペアによるリスト初期化(list initialization)では意図しない型推論が行われる。かなり罠っぽい挙動。

// C++1z(C++17)
#include <vector>  // std::vector<T>

std::vector v0{ 1, 2, 3, 4 };
// vector<int> に推論される
// コンストラクタvector(intializer_list<int>)を選択
// v0 = 4要素[1, 2, 3, 4]

std::vector v1( v0.begin(), v0.end() );
// 後述deduction guideによって
//   T = iterator_traits<vector<int>::iterator>::value_type
//   つまり vector<int> に推論される
// コンストラクタvector(InputIterator, InputIterator)を選択
// v1 = 4要素[1, 2, 3, 4]

std::vector v2{ v0.begin(), v0.end() };  // ★
// vector<vector<int>::iterator> に推論される
// コンストラクタvector(intializer_list<vector<int>::iterator>)を選択
// v2 = 2要素[v0.begin(), v0.end()]

std::vector<T>クラステンプレートが提供する推論ガイド*1は次の通り。2個のイテレータ引数(InputIterator型)からコンテナ要素型Tを導く推論ガイドであり、std::deque, std::forward_list, std::listに対しても同じ目的の推論ガイドがそれぞれ提供される。

// <vector>ヘッダ
namespace std {
  template<class InputIterator>
    vector(InputIterator, InputIterator)
      -> vector<typename iterator_traits<InputIterator>::value_type>;
}

一方で、std::vector<T>をはじめとするシーケンスコンテナは、単一のstd::initializer_list<T>型をとるコンストラクタ*2を提供する。これによって変数v0のような統一初期化記法が可能となっているが、「std::initializer_list<T>コンストラクタは他コンストラクタよりも優先される」オーバーロード規則によって変数v2のような(プログラマの意図に反するであろう)型推論が行われる。*3

N4659(C++1z DIS) 16.3.1.7/p1, 16.3.1.8/p2より一部引用。

When objects of non-aggregate class type T are list-initialized such that 11.6.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (11.6.4) of the class T and the argument list consists of the initializer list as a single argument.
  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

(snip)

Initialization and overload resolution are performed as described in 11.6 and 16.3.1.3, 16.3.1.4, or 16.3.1.7 (as appropriate for the type of initialization performed) for an object of a hypothetical class type, where the selected functions and function templates are considered to be the constructors of that class type for the purpose of forming an overload set, and the initializer is provided by the context in which class template argument deduction was performed. (snip)

関連URL

*1:本文中では簡単のためアロケータAllocatorを省略した。厳密な定義はN4659 26.3.11.1を参照のこと。

*2:正確にはvector::vector(initializer_list<T>, const Allocator& = Allocator())となっている。

*3:関連する話題としてP0702R1が提案・採択されている。vector v{vector{1, 2}};と書いたとき、const vector<T>&コピーコンストラクタよりも、initializer_list<vector<T>>コンストラクタが優先されてしまう問題への対処。

2017-09-04

C++ Concepts(P0734R0)

次期C++2a(C++20)標準仕様に向けて採択された コンセプト(concept) についてメモ。*1

本記事の内容は(PDF)P0734R0 Wording Paper, C++ extensions for Conceptsに基づく。

要約:

  • 新しいキーワード:concept, requires
  • 新しい構文:コンセプト定義, requires式, requires節
  • コンセプト(concept) == テンプレートパラメータに対する制約(constraint)
  • コンセプト(concept) != 型(type)
  • 関数オーバーロード解決では、関数に関連付けられた制約(associated constraints)が考慮される。("concept-based overloading")
  • 注意:P0734R0はコンセプトに関するC++言語仕様のみを規定し、具体的なコンセプトは何も定義しない。コンセプトライブラリは"Ranges TS"*2で検討が行われている。

コンセプト(concept)

「コンセプト(concept)」は、C++の新しいエンティティ(entity)として追加される。コンセプトは“ある型が満たすべき制約(constraint)の集合”に名前をつけたものであり、新キーワード concept を用いてテンプレート定義によく似た構文で定義される。コンセプト名に型を指定したものは、bool型のprvalueに評価される。*3

#include <type_traits>  // is_integral_v

// ある型パラメータT に対する コンセプトC0 を定義
template <typename T>
concept C0 = std::is_integral_v<T>;  // 型Tは整数型であること
// 型パラメータパックTs に対する コンセプトCV を定義
template <typename... Ts>
concept CV = sizeof...(Ts) == 2;     // パックTsの要素数が2であること

static_assert( C0<char> );     // OK
static_assert( !C0<double> );  // OK
static_assert( CV<float,double> );  // OK
static_assert( !CV<int,int,int> );  // OK

コンセプト定義(concept-definition)では、コンセプトのプロトタイプパラメータ(prototype parameter)に対する制約式(constraint-expression)を指定する*4。制約式では &&|| を用いて、制約に対する論理演算*5を指定できる。

template <typename T> concept A = /* 制約式 */;  // コンセプトA
template <typename T> concept B = /* 制約式 */;  // コンセプトB

// コンセプトC: 型TはコンセプトAかつコンセプトBの両方を満たす
template <typename T> concept C = A<T> && B<T>;
// コンセプトD: 型TがコンセプトCを満たす または 型UがコンセプトCを満たす
template <typename T, typename U> concept D = C<T> || C<U>;

ノート:C++14現在のISO/IEC TS 19217:2015(通称"Concepts TS")では、コンセプト定義は変数テンプレートや関数テンプレート風の構文であった。Concepts TSではキーワードbool明示が必要だが、C++2aコンセプトでは上記のように専用構文となるためbool不要となる。

// Concepts TS: 変数テンプレートの構文
template <typename T> concept bool C0 = sizeof(T) == 1;
// Concepts TS: 関数テンプレートの構文
template <typename T> concept bool C0() { return sizeof(T) == 1; }

requires式による要件(requirement)表現

コンセプト定義に与える制約式では、新キーワード requires を用いた「requires式(requires-expression)」が利用できる。

requires式は 1)“ある式が有効”、2)“ある型が存在する”、3a)“ある式が有効で、評価結果は特定の型に変換可能”、3b)“ある関数呼出しが有効で、例外送出しない”、3c)“ある式が有効で、評価結果は別コンセプトを満たす型に推論可能”、4)“ある型が別コンセプトを満たす”といった要件(requirement)を、requires式の本体部に複数個並べて表現する。全ての要件を満たす(satisfied)場合にかぎり、requires式はtrueとなる。(requires式の本体部が実行されるわけではない)

// 1) 型T に対する制約を表す コンセプトC1
template <typename T> concept C1 =
  requires (T a, T b) {  // 型Tの2変数a, bに対して...
    a + b;  // 型Tの2変数に対する加算演算+が有効
    a - b;  // 型Tの2変数に対する減算演算-が有効
  };

// 2) 型T に対する制約を表す コンセプトC2
template <typename T> concept C2 = 
  requires {  // (requires変数リストは省略可)
    typename T::inner;  // 型Tは入れ子の型T::innerを持つ
    typename S<T>;      // クラステンプレートSの特殊化S<T>が有効
  };

// 3) 型T に対する制約を表す コンセプトC3
template <typename T> concept C3 =
  requires (T x) {  // 型Tの変数xに対して...
    { !x } -> bool;
    // a) 型Tの変数に対する単項否定演算!が有効
    // かつ その評価結果はbool型に変換可能

    { f(x) } noexcept;
    // b) 式f(x)が有効 かつ 例外送出しない
    { g(x) } noexcept -> int;
    // b) 式g(x)が有効 かつ 例外送出せず
    // かつ その評価結果はint型へ変換可能

    { h(x) } -> const C2&;
    // c) 式h(x)が有効 かつ その評価結果は架空の
    // テンプレート関数Fへの実引数として指定できる
    //   template<C2 U> void F(const U&);
  };

// 4) 入れ子のコンセプトを含む コンセプトC4
template <typename T> concept C4 =
  requires {
    requires C3<T>;  // 型TはコンセプトC3を満たす
    //...
  };

requires式の本体部における構文では、下記の要件(requirement)を記述可能となっている(簡略版):

  • 1) simple-requirement: ;
  • 2) type-requirement: typename 型名 ;
  • 3) compound-requirement:
    • {} noexceptopt ;
    • {} noexceptopt -> 型名 ;
    • {} noexceptopt -> 制約付きパラメータ(constrained-parameter) ;
  • 4) nested-requirement: requires 制約式(constraint-expression) ;

テンプレートへの制約の関連付け

テンプレート宣言/定義においてテンプレートパラメータにコンセプトを適用するため、3種類の構文が追加される:(Cは関数テンプレートに対してのみ有効)

  • A) template宣言のテンプレートパラメータリストにおいて、typename/classキーワードや型名の代わりに コンセプト名 を用いる。*6
  • B) template宣言に続いて「requires節(requires-clause)」を指定する。
  • C) 関数テンプレートのプロトタイプ末尾に「末尾requires節(trailing requires-clause)」を指定する。
// 型パラメータTに対する コンセプトC を定義
template <typename T> concept C = /*...*/;

// 通常の関数テンプレート
template <typename T>  // 型テンプレートパラメータT; 制約なし
void func0t(T);
template <int N>       // 非型テンプレートパラメータN; 制約なし
void func0n();

// A) 制約付きパラメータ(constrained-parameter)による制約
template <C T> void funcA(T);
// B) requires節(requires-clause)による制約
template <typename T> requires C<T> void funcB(T);
// C) 末尾requires節(trailing requires-clause)による制約
template <typename T> void funcC(T) requires C<T>;

funcA, funcB, funcCいずれの関数テンプレートも、制約 C<T> が関連付けられる。制約は複数指定することもでき、下記funcDは制約 C1<T>∧C2<T>∧C3<T> が関連付けられる。

template <typename T> concept C1 = /*...*/;
template <typename T> concept C2 = /*...*/;
template <typename T> concept C3 = /*...*/;

template<C1 T> void funcD(T) requires C2<T> && C3<T>;

関数テンプレートに対して“関連付けられた制約(associated constraints)”が存在する場合、その制約が満たされた(satisfied)ときに限って関数オーバーロード候補に列挙される。また、異なる制約間には半順序(partial ordering)が定義され、関数オーバーロード解決の順序に影響を与える。関数オーバーロード解決の詳細仕様はヽ(・∀・ヽ) 理解!! (ノ・∀・)ノ 不能!!

// P0734R0 17.10.4/p5 Example
template<typename T> concept C1 = requires(T t) { --t; };
template<typename T> concept C2 = C1<T> && requires(T t) { *t; };

template<C1 T> void f(T);       // #1
template<C2 T> void f(T);       // #2
template<typename T> void g(T); // #3
template<C1 T> void g(T);       // #4

f(0);        // selects #1
f((int*)0);  // selects #2
g(true);     // selects #3 because C1<bool> is not satisfied
g(0);        // selects #4

関連URL

*1:広く合意された対訳語は存在しないため、本文中では日本語直訳+英語表記を併記している。

*2http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4685.pdf

*3:P0734R0でも"evaluate(評価)"と表現しているが、実行時ではなくコンパイル時に行われる処理。またコンセプト(concept)はインスタンス化されないため、制約 C0<char> に対応する実体が存在するわけではない。

*4:P0734R0 17.5.6/p1では"A concept is a template that defines constraints on its template arguments."と定義されており、コンセプトはテンプレートの一種となっている。プロトタイプパラメータ(prototype parameter)=コンセプト定義のテンプレートパラメータ。

*5:ここでの && は合接(conjunction; ∧)、|| は離接(disjunction; ∨)とよばれる論理演算。この“制約に対する論理演算”は通常の論理演算子(logical OR/AND)とは別物として定義されるが、実用上は同義との理解で差し支えないはず。

*6:本文中では説明のため短いコンセプト名(Cなど)を用いている。実用上は制約条件を適切に表現でき、かつコンセプト(concept)であることが読み取れる名前をつけること。

2017-08-26

Underscore As A Keyword

Java 9では1文字アンダースコア(_)はキーワード(keyword)に変更され、変数名などに利用するとコンパイルエラーになる。Java 8以前は非推奨の識別子(identifier)であり、コンパイル時の警告メッセージにとどまっていた。

class UAAK {
  public static void main(String[] args) {
    int _ = 42;
  }
}

Java 8コンパイル警告メッセージ:

warning: '_' used as an identifier
(use of '_' as an identifier might not be supported in releases after Java SE 8)

Java 9コンパイルエラー:

error: as of release 9, '_' is a keyword, and may not be used as an identifier

関連URL

2017-08-20

Designated Initialization @ C++

C++2a(C++20)で導入予定のDesignated Initialization(指示付きの初期化)ついてメモ。C99言語での同一言語機能をベースとするが、一部機能制限された形でC++に導入される。

// C++2a(C++20)
struct Point { int x, y; };
void f(Point);

f({ .x = 100, .y = 50 });  // Designated Initialization

// 指示子順==メンバ宣言順 が条件
f({ .y = 50, .x = 100 });  // C++2aではNG(C99ではOK)

C++2a言語機能とC99言語機能の差異は下表の通り(P0329R0より引用)。特に初期化子順序をデータメンバ宣言順と一致させる必要がある点に注意。関数呼び出しの名前付きパラメータ(named parameter)的な利用方法には使いづらそう。

C++2aC99コード例
指示子順とメンバ宣言順の一致が必要指示子順序は任意C99ではOK/C++2aではNG
struct { int a, b; };
A a = { .b = 3, .a = 4 };
指示付き初期化の評価順序は左から右初期化の評価順序は未規定*1 *2
全て指示付き、または全て指示無し混在可能C99ではOK/C++2aではNG
A a = { 3, .a = 4 };
指示付き初期化は重複不可重複可能C99ではOK/C++2aではNG
A a = { .a = 3, .a = 4 };
配列型には未対応配列要素の初期化に対応C99ではOK/C++2aではNG
A a = { [3] = 4 };
指示付き初期化のネスト不可ネスト対応C99ではOK/C++2aではNG
A a = { .e.a = 3 };
統一初期化に対応(Cの初期化子のみ)C++2aのみOK
A a = { .a{} };

Working Draft N4687 C.1.7, 11.6.1より引用。

Change: In C++, designated initialization support is restricted compared to the corresponding functionality in C. In C++, designators for non-static data members must be specified in declaration order, designators for array elements and nested designators are not supported, and designated and non-designated initializers cannot be mixed in the same initializer list.
Example:

struct A { int x, y; };
struct B { struct A a; };
struct A a = {.y = 1, .x = 2};  // valid C, invalid C++
int arr[3] = {[1] = 5};         // valid C, invalid C++
struct B b = {.a.x = 0};        // valid C, invalid C++
struct A c = {.x = 1, 2};       // valid C, invalid C++

Rationale: In C++, members are destroyed in reverse construction order and the elements of an initializer list are evaluated in lexical order, so field initializers must be specified in order. Array designators conflict with lambda-expression syntax. Nested designators are seldom used.
Effect on original feature: Deletion of feature that is incompatible with C++.
Difficulty of converting: Syntactic transformation.
How widely used: Out-of-order initializers are common. The other features are seldom used.

N4687 Working Draft, Standard for Programming Language C++

関連URL

*1:C99 6.7.8/p23: The order in which any side effects occur among the initialization list expressions is unspecified.

*2:C11 6.7.9/p23: The evaluations of the initialization list expressions are indeterminately sequenced with respect to one another and thus the order in which any side effects occur is unspecified.

2017-08-17

std::launder関数

C++1z(C++17)標準ライブラリに追加される std::launder 関数テンプレートについて。メモリ・ロンダリング関数。*1

まとめ:

  • オブジェクト生存期間(lifetime)に基づいた最適化の抑止をコンパイラに伝える関数。プログラマ視点では“何もしない関数”にみえる。*2
  • 配置new(placement new)利用時のみ役立つ関数。アプリケーション開発者向けの機能ではなく、メモリアロケータやライブラリ内部実装者向け。
  • C++怖い。

A language support tool (an "optimisation barrier") to allow libraries to reuse storage and access that storage through an old pointer, which was previously not allowed. (This is an expert tool for implementers and not expected to show up in "normal" code.)

P0636R0 Changes between C++14 and C++17 DIS

21.6.4 Pointer optimization barrier
template <class T> constexpr T* launder(T* p) noexcept;

1 Requires: p represents the address A of a byte in memory. An object X that is within its lifetime (6.8) and whose type is similar (7.5) to T is located at the address A. All bytes of storage that would be reachable through the result are reachable through p (see below).
2 Returns: A value of type T * that points to X.
3 Remarks: An invocation of this function may be used in a core constant expression whenever the value of its argument may be used in a core constant expression. A byte of storage is reachable through a pointer value that points to an object Y if it is within the storage occupied by Y, an object that is pointer-interconvertible with Y, or the immediately-enclosing array object if Y is an array element. The program is ill-formed if T is a function type or cv void.
4 [Note: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless the type contains constor reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object. See 6.8. -- end note]
5 [Example:

struct X { const int n; };
X *p = new X{3};
const int a = p->n;
new (p) X{5};  // p does not point to new object (6.8) because X::n is const
const int b = p->n;                // undefined behavior
const int c = std::launder(p)->n;  // OK

-- end example]

N4659 Working Draft, Standard for Programming Language C++

std::launder関数の効果は下記サンプルコードを参照。「参照型やconstデータメンバを含む構造体/クラスを扱う」または「汎用メモリストレージを具体的な型にキャストする」とき、配置newとの組み合わせ利用には細心の注意を払うこと。

// constデータメンバを"含む"構造体
struct X {
  const int n;
  int m;
};

X *p = new X{3};
new(p) X{5};  // 配置new(戻り値ポインタは破棄)

// pはlifetimeが切れたオブジェクトを指しているため
// 下記コードは"いずれも"未定義動作を引き起こす。
int a0 = p->n;  // NG
int b0 = p->m;  // NG: 非constメンバもダメ

// std::launder(p)は配置newによる新しいオブジェクトを指すため
// 下記コードはいずれもwell-definedとなる。
int a1 = std::launder(p)->n;  // OK: C++1z
int b1 = std::launder(p)->m;  // OK: C++1z
std::aligned_storage<sizeof(int), alignof(int)>::type data;
int* ptr = new(&data) int{42};

// dataとintは異なる型のため、launder関数を通す必要がある
int x1 = *reinterpret_cast<int*>(&data);                // NG
int x2 = *std::launder(reinterpret_cast<int*>(&data));  // OK: C++1z

// 配置new式が返したポインタptrは、必ず新しいオブジェクトを指す
int x0 = *ptr;  // OK

N4659(C++1z DIS) 4.5/p2, 6.8/p8より引用(下線部は強調)。

Objects can contain other objects, called subobjects. A subobject can be a member subobject (12.2), a base class subobject (Clause 13), or an array element. An object that is not a subobject of any other object is called a complete object. If an object is created in storage associated with a member subobject or array element e (which may or may not be within its lifetime), the created object is a subobject of e's containing object if:

  • the lifetime of e's containing object has begun and not ended, and
  • the storage for the new object exactly overlays the storage location associated with e, and
  • the new object is of the same type as e (ignoring cv-qualification).

[Note: If the subobject contains a reference member or a const subobject, the name of the original subobject cannot be used to access the new object (6.8). -- end note] [Example:

struct X { const int n; };
union U { X x; float f; };
void tong() {
  U u = {{ 1 }};
  u.f = 5.f;                           // OK, creates new subobject of u (12.3)
  X *p = new (&u.x) X {2};             // OK, creates new subobject of u
  assert(p->n == 2);                   // OK
  assert(*std::launder(&u.x.n) == 2);  // OK
  assert(u.x.n == 2);                  // undefined behavior, u.x does not name new subobject
}

-- end example]

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (4.5) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

(snip) [Note: If these conditions are not met, a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std::launder (21.6). -- end note]

メモ:P0532R0によればstd::launderの導入だけでは、std::vector<T>をはじめとするアロケータ・サポートのある標準コンテナで、const修飾もしくは参照型データメンバを含むクラスTを正しく扱えないという問題が残るようにも読める。

関連URL

*1http://ejje.weblio.jp/content/launder wikipedia:資金洗浄

*2:関数入出力で型も値も変化しない恒等変換となっている。コンパイラへの指示だけを目的とする関数として、他に std::kill_dependency が存在する。