Rokiのチラ裏

学生による学習のログ

stack overflow からの小ネタメモ #3

前回

stackoverflow.com C++98(03) ではstd::vectorの fill コンストラクタのプロトタイプに初期化子のデフォルト値があるが C++11 では削除されている。 C++03 – ISO 14882:2003 23.2.4.1 vector constructors, copy, and assignment [lib.vector.cons] から一部引用。

explicit vector(size_type n, const T& value = T(),const Allocator& = Allocator());

続いて N3337 23.3.6.2 vector constructors, copy, and assignment [vector.cons] から一部引用。

vector(size_type n, const T& value,const Allocator& = Allocator());

C++98(03) はプロトタイプオブジェクトを取り出し、それをn回コピーした。C++11では、n個のデフォルトオブジェクトが構築されるようになった事で、n個のコピーが削除され、n個のデフォルト構成に置き換えられる事となった。また、copy-constructible を要求する必然性はないのでそれが排除された。

stackoverflow.com

他のコンストラクターが有効でない場合にのみ呼び出される、variadic forwarding reference constructor の SFINAE 定義において以下のような解決策が講じられた。gcc 5.2.0 と clang 3.7.0 では意図した通りに動くが、これはどのように動作するのか、また合法か否か。

template <typename... T,typename = std::enable_if_t<!std::is_constructible<C, T&&...>::value>>
C(T&&... ) { }

現在の作業草案では、デフォルトのテンプレート引数がインスタンス化されるタイミングは記述されていない*1*2。興味深いのは、LWG 2452 でもあるようにそのセマンティックスを無視するかのような動作をする事だ。

struct C {
  C(const char*, size_t) {}
  template <class... Ts,
    typename = std::enable_if_t<!std::is_constructible<C, Ts&&...>::value>
  >
  C(Ts&&... ) { }
};

int main() {
  int a = 0;
  C x{a};
  std::cout << std::is_constructible<C, int&>{} << '\n'; // 0
}

これは、上述の理由により UB を引き起こす。LWG 2452 には Library specification でその旨を明記するよう呼びかけられている。

stackoverflow.com C++17 で<new>ヘッダに追加されたstd::launderの目的とは何か。コンパイラC++ のライフタイム規則*3に基づく最適化を行えるが、定数メンバでオブジェクトを再初期化する(placement new)と、それらの規則と上手く両立できない。std::launderは、ライフタイムルールとは無関係に、同じアドレスに以前存在したオブジェクトではなくアドレスが新しいオブジェクトのみを参照することができるコンパイラへのヒントである。オブジェクトモデルの観点からは、あるアドレスのある潜在的に寿命の長いオブジェクトへのポインタから、そのアドレスの現在有効なオブジェクトへのポインタへのポインタをマッピングする。実装の観点から見ると、コンパイラは、返されたポインタのバージョンが指すオブジェクトのアイデンティティについて何も知らないと仮定することを防ぐ事で最適化の障壁となる*4N4659 [ptr.launder] では*5、これを pointer optimazation barrier と称している。N4659 21.6.4 Pointer optimization barrier [ptr.launder] から引用。

template <class T> constexpr T* launder(T* p) noexcept;
Requires: p represents the address A of a byte in memory. An object X that is within its lifetime and whose type is similar 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).

Returns: A value of type T * that points to X.

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.

[ 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 const or reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object. See [basic.life]. — end note ]

[ 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 ([basic.life]) because X<200b>::<200b>n is const.
const int b = p->n;                 // undefined behavior
const int c = std::launder(p)->n;   // OK
— end example ]

尚引用にもある通り T が関数型または(及びcv修飾された)voidの場合、ill-formed である。引数の値が core constant expression で使用される場合、std::launderを core constant expression で使用することができる。また、constメンバーまたは参照メンバーを持たないオブジェクトの場合、lifetime ルールに基づきポインタと参照を再利用できるためstd::launderは必要ない。 続いて p0636r0 から Examples, notes を引用

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.)

ライブラリが以前許可していなかった古いポインタを使用してストレージを再利用し、そのストレージにアクセスできるようにする言語サポートツール(“最適化バリア”)。 これは実装者のための専門ツールであり、"通常の"コードでは見られないとある。続いて関連項目を引用。N4659 4.5 The C++ object model [intro.object] paragraph 1 から。

The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]). An object occupies a region of storage in its period of construction ([class.cdtor]), throughout its lifetime, and in its period of destruction ([class.cdtor]). [ Note: A function is not an object, regardless of whether or not it occupies storage in the way that objects do. — end note ]

続いて N4659 [intro.object] paragraph 2

Objects can contain other objects, called subobjects. A subobject can be a member subobject ([class.mem]), a base class subobject ([class.derived]), 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 ([basic.life]). — 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 ([class.union])
  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 ]

以下は例。

#include <new>
 
struct X {
  const int n; // note: X has a const member
  int m;
};
int main()
{
  X *p = new X{3};
  const int a = p->n;
  new (p) X{5};       // p does not point to new object because X::n is const
  const int b = p->n; // undefined behavior
  const int x = p->m; // undefined behavior (even though m is non-const, p can't be used)
  const int c = std::launder(p)->n; // OK, std::launder(p) points to new object
}
#include <iostream>

int main() 
{
   alignas(int) char data[sizeof(int)];
   int *myInt = new (data) int;
   *myInt = 34;

   std::cout << *reinterpret_cast<int*>(data); // dereferencing type-punned pointer will break strict-aliasing rules 
   std::cout << *std::launder(reinterpret_cast<int*>(data)); // OK
}

std::launderにおける feature test macro は __cpp_lib_launder である。

*1:CWG 2008

*2:尚この問題は、is_constructible + default 関数/テンプレート引数の唯一の問題ではない。例えば、LWG2452を参照されたい。

*3:[basic.life]/paragraph 8

*4:https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/ko5ceM4szIE

*5:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4430.htmlによる追加