yohhoyの日記

技術的メモをしていきたい日記

yield式を使わないジェネレータ

C++2a(C++20)コルーチンにはジェネレータ実装を容易にするco_yield式が導入されるが、動作仕様的にはco_await式のシンタックスシュガーとなっている。

#include <coroutine>
#include <iostream>
#include <utility>

#define MIMIC_CO_YIELD 1

#if MIMIC_CO_YIELD
// yield式相当を表現する値保持クラス
template <typename T> struct yield { T value; };
#endif

template <typename T>
struct generator {
  struct promise_type {
    T value_;
    auto get_return_object() { return generator(*this); }
    auto initial_suspend() { return std::suspend_always{}; }
    auto final_suspend() { return std::suspend_always{}; }
#if MIMIC_CO_YIELD
    // 式co_await yield{v}に対応するカスタイマイズポイント
    auto await_transform(yield<T>&& bag) {
      value_ = std::move(bag.value);
      return std::suspend_always{};
    }
#else
    auto yield_value(T x) {
      value_ = std::move(x);
      return std::suspend_always{};
    }
#endif
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
  };
  using coro_handle = std::coroutine_handle<promise_type>;

  generator(generator const&) = delete;
  generator(generator&& rhs) : coro_(rhs.coro_) { rhs.coro_ = nullptr; }
  ~generator() { if (coro_) coro_.destroy(); }
 
  bool next() { return coro_ ? (coro_.resume(), !coro_.done()) : false; }
  T& value() { return coro_.promise().value_; }

private:
  generator(promise_type& p)
    : coro_(coro_handle::from_promise(p)) {}
  coro_handle coro_;
};

generator<int> f()
{
#if MIMIC_CO_YIELD
  // co_await式を利用
  co_await yield{1};
  co_await yield{2};
#else
  // 通常のco_yield式
  co_yield 1;
  co_yield 2;
#endif
}

int main() {
  auto g = f();
  while (g.next()) {
    std::cout << g.value() << std::endl;
  }
}

C++2a WD(n4861) 7.6.17/p1より一部引用。

A yield-expression shall appear only within a suspension context of a function (7.6.2.3). Let e be the operand of the yield-expression and p be an lvalue naming the promise object of the enclosing coroutine (9.5.4), then the yield-expression is equivalent to the expression co_await p.yield_value(e).
(snip)

関連URL

C++20標準ライブラリ仕様:Constraints/Mandates/Preconditions

C++2a(C++20)標準ライブラリの関数仕様記述で用いられる Constraints/Mandates/Preconditions の違いについてメモ。

  • 現行C++17標準ライブラリの Requires は廃止され、C++2aでは Constraints/Mandates/Preconditions に細分化される。
  • Mandates: 型や定数式に対する必須要件。違反時は ill-formed のためコンパイルエラー。
  • Constraints: 型や定数式に対する制約条件。違反時は関数オーバーロード候補から除外される。“SFINAE-friendly”
  • Preconditions: 引数値やオブジェクト状態に対する事前条件。違反時は実行時エラーや未定義動作(undefined behavior)を引き起こす。
  • C++標準規格では関数仕様のみを定め、標準ライブラリの実現方式には言及しない。
    • 例:Constraintsは requires節*1enable_if*2、constexpr if*3 など任意の仕組みで実現されうる。

例:std::packaged_task<R(ArgTypes...)>のテンプレートコンストラクtemplate<class F> packaged_task(F&& f);定義は、C++17/C++2a標準ライブラリ仕様ではそれぞれ下記の通り記述される。*4

C++17仕様)
Requires: INVOKE<R>(f, t1, t2, ..., tN), where t1, t2, ..., tN are values of the corresponding types in ArgTypes..., shall be a valid expression. Invoking a copy of f shall behave the same as invoking f.
Remarks: This constructor shall not participate in overload resolution if decay_t<F> is the same type as packaged_task<R(ArgTypes...)>.

C++2a仕様)
Constraints: remove_cvref_t<F> is not the same type as packaged_task<R(ArgTypes...)>.
Mandates: is_invocable_r_v<R, F&, ArgTypes...> is true.
Preconditions: Invoking a copy of f behaves the same as invoking f.

提案文書(PDF)P0788R3, §3 Proposed principles and practices より一部引用(下線部は強調)。注:C++2aではContracts導入が見送られたため*5Expects:Ensures: はそれぞれ Preconditions:Postconditions: が対応する。

I. Let's not recycle a Requires: element to mean something other than what it means today.

  • a) Let's instead adopt new elements, described below, to specify the Library requirements that are (or that should have been) specified via our current Requires: elements. [(snip)]
  • (snip)


II. Let's introduce a new Constraints: element.

  • a) Let's use this Constraints: element to specify the compile-time circumstances that must be satisfied in order that the corresponding Library component will be compiled. [(snip)]
  • b) Let's ensure that unsatisfied Constraints: not produce any diagnostic in and of themselves.
    [This obviates the need for specification wording such as "shall not participate in overload resolution." Note that a consequential diagnostic might still result: for example, overload resolution might find no viable candidates due to unsatisfied constraints and/or other factors.]
  • c) Let's introduce a new Mandates: element to specify the compile-time circumstances under which, when unsatisfied, an implementation must produce a diagnostic.
    [(snip)]
    [This element obviates the need for any "is ill-formed" specifications. For example, in [pair.astuple]/1 we today find the specification "Requires: I < 2. The program is ill-formed if I is out of bounds." Under the present proposal, this would be simplified to "Mandates: I < 2."]


III. Let's introduce a new Expects: element.

  • a) Let's use this Expects: element to specify the circumstances that must be satisfied to avoid undefined behavior when the corresponding Library component is invoked.
    [Industry-wide, such requirements have come to be known as preconditions, but the Contracts proposals [P0542R1] seem to have chosen "expects" as their preferred term of art; it seems better to have a single term and use it consistently.]
  • (snip)


IV. Let's avoid any specification that demands any particular technology by which implementations must comply with Library specifications.

  • a) Let's permit an implementation to use a requires-clause, an enable_if, a constexpr if, or any other technology or combination of technologies to meet Constraints: specifications.
  • b) Let's permit an implementation to use static_assert and/or any other technologies to meet Mandates: specifications.
  • c) Let's permit an implementation to use Contracts attributes [P0542R1] and/or any other technologies to meet Expects: and Ensures: specifications.
  • d) Let's consider user code that relies on any specific technology on the part of an implementation to be ill-formed, with no diagnostic required.

C++17仕様

C++17 20.4.1.4/p3-4より一部引用。

3 Descriptions of function semantics contain the following elements (as appropriate):

  • Requires: the preconditions for calling the function
  • (snip)

4 (snip) If F's semantics specifies a Requires: element, then that requirement is logically imposed prior to the equivalent-to semantics. (snip)

C++2a仕様

C++2a DIS(N4861) 16.4.1.4/p3より一部引用。

3 Descriptions of function semantics contain the following elements (as appropriate):

  • Constraints: the conditions for the function's participation in overload resolution (12.4). [Note: Failure to meet such a condition results in the function's silent non-viability. --end note] [Example: An implementation might express such a condition via a constraint-expression (13.5.2). --end example]
  • Mandates: the conditions that, if not met, render the program ill-formed. [Example: An implementation might express such a condition via the constant-expression in a static_assert-declaration (9.1). If the diagnostic is to be emitted only after the function has been selected by overload resolution, an implementation might express such a condition via a constraint-expression (13.5.2) and also define the function as deleted. --end example]
  • Preconditions: the conditions that the function assumes to hold whenever it is called.
  • (snip)

4 (snip) If F's semantics specifies any Constraints or Mandates elements, then those requirements are logically imposed prior to the equivalent-to semantics. (snip)

関連URL

atoi関数のかしこい実装

C標準ライブラリ Muslのatoi関数実装 では、符号付き整数オーバーフロー回避のため負数範囲で10進数値を減算してゆき最後に符号反転を行っている。

int atoi(const char *s)
{
  int n=0, neg=0;
  while (isspace(*s)) s++;
  switch (*s) {
  case '-': neg=1;
  case '+': s++;
  }
  /* Compute n as a negative number to avoid overflow on INT_MIN */
  while (isdigit(*s))
    n = 10 * n - (*s++ - '0');
  return neg ? n : -n;
}

C言語の符号付き整数型(int)では “2の補数” 表現が用いられるため*1、最大値INT_MAXより最小値INT_MINの絶対値が 1 だけ大きくなり(例:16bit幅ならINT_MAX = 32767, INT_MIN = -32768)、正数範囲の累積計算では値-INT_MINを表現できず未定義動作(undefined behavior)を引き起こしてしまう。*2

その他のC標準ライブラリ実装(glibc, BSD libc, Newlib)では、単純にstrtol関数へ実装移譲している。

int atoi (const char *s)
{
  return (int) strtol (s, NULL, 10);
}

ノート:int型よりlong型の値域が広い場合strtol関数戻り値キャストで未定義動作(undefined behavior)となるが、そもそもC標準ライブラリatoi関数仕様では「戻り値がint型の値域外となる場合の動作は未定義(the behavior is undefined)」とされるため何ら問題はない。いいね?

C99 3.4.3/p3, 7.20.1/p1, 7.20.1.4/p8より引用(下線部は強調)。

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undefined.

The strtol, strtoll, strtoul, and strtoull functions return the converted value, if any. If no conversion could be performed, zero is returned. If the correct value is outside the range of representable values, LONG_MIN, LONG_MAX, LLONG_MIN, LLONG_MAX, ULONG_MAX, or ULLONG_MAX is returned (according to the return type and sign of the value, if any), and the value of the macro ERANGE is stored in errno.

関連URL

*1:C17現在は “1の補数(ones' complement)” または “符号と絶対値(sign and magnitude)” 表現も許容されるが、次期規格C2xでは “2の補数(two's complement)” のみ許容と改定される。提案文書(PDF)N2412参照。

*2:符号付き整数オーバーフローは、標準規格での明示的な定義が無いことをもって未定義動作とされる(C99 4/p2)

関数型への参照型にまつわる特例ルール

プログラミング言語C++の関数型(function type)には左辺値(lvalue)しか存在しないが(→id:yohhoy:20200530)、左辺値参照型(R (&)(Args...))/右辺値参照型(R (&&)(Args...))いずれにも束縛できる。そう、よかったね。

#include <utility>
using std::move;  // 右辺値参照型(T&&)を返す関数

void f() {}
// 関数型void()
void ( &lref1)() = f;        // OK
void (&&rref1)() = f;        // OK: 右辺値参照型だがlvalue束縛可能
void ( &lref2)() = move(f);  // OK: 関数型の式move(f)はlvalue
void (&&rref2)() = move(f);  // OK: 右辺値参照型だがlvalue束縛可能
// (関数型のprvalueは存在しない)

int n = 42;
// 通常の型(int)
int&  lref3 = i;        // OK
int&& rref3 = i;        // NG: 右辺値参照型はlvalue束縛不可
int&  lref4 = move(i);  // NG: 左辺値参照型はxvalue束縛不可
int&& rref4 = move(i);  // OK
int&  lref5 = 42;       // NG: 左辺値参照型はprvalue束縛不可
int&& rref5 = 42;       // OK

関数オーバーロード解決では関数型への左辺値参照型が優先されるため、あいまいさは存在しない。(C++17 16.3.3.2/p3)

C++17 11.6.3/p5より一部引用(下線部は強調)。

A reference to type "cv1 T1" is initialized by an expression of type "cv2 T2" as follows:

  • If the reference is an lvalue reference and the initializer expression
    • is an lvalue (but is not a bit-field), and "cv1 T1" is reference-compatible with "cv2 T2", or
    • (snip)
  • Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be const), or the reference shall be an rvalue reference. (snip)
    • If the initializer expression
      • is an rvalue (but not a bit-field) or function lvalue and "cv1 T1" is reference-compatible with "cv2 T2", or
      • (snip)

関数型の右辺値は存在しない

プログラミング言語C++に「関数型(function type)の右辺値(rvalue)」は存在しない。関数型をもつ式の value category は常に左辺値(lvalue)となる。だから何?

int v;
struct S s;
void f();

std::move(v);  // xvalue
std::move(s);  // xvalue
std::move(f);  // lvalue

static_cast<void(&)()>(f);   // lvalue
static_cast<void(&&)()>(f);  // lvalue
static_cast<void(*)()>(f);   // prvalue(関数ポインタ型)
void g(void(&)());   // #1
void g(void(&&)());  // #2

g(f);  // #1を呼び出す
g(std::move(f)); // #1を呼び出す

別途 Function-to-pointer 変換によって、関数型の lvalue は関数ポインタ型の prvalue へと変換されうる。*1

C++17 7/p6, 8.2.2/p11 より引用(下線部は強調)。*2

The effect of any implicit conversion is the same as performing the corresponding declaration and initialization and then using the temporary variable as the result of the conversion. The result is an lvalue if T is an lvalue reference type or an rvalue reference to function type (11.3.2), an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. The expression e is used as a glvalue if and only if the initialization uses it as a glvalue.

A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.

C++11に右辺値参照が追加されたとき、新しい概念の導入による複雑化を避けるため特別扱いされた。(PDF)N3055 Background より一部引用。

In addition, rvalue references (like traditional lvalue references) can be bound to functions. Treating an rvalue reference return value as an rvalue, however, introduces the novel concept of a function rvalue into the language. There was previously no such idea -- a function lvalue used in an rvalue context becomes a pointer-to-function rvalue, not a function rvalue -- so the current draft Standard does not describe how such rvalues are to be treated. In particular, function calls and conversions to function pointers are specified in terms of function lvalues, so most plausible uses of rvalue references to functions are undefined in the current wording.

関連URL

*1:C++17 7.3/p1: "An lvalue of function type T can be converted to a prvalue of type "pointer to T". The result is a pointer to the function."

*2:static_cast式, reinterpret_cast式, キャスト式でも rvalue reference to function type へのキャスト結果は lvalue と定義されている(8.2.9/p1, 8.2.10/p1, 8.4/p1)

C2x標準の属性(attribute)

プログラミング言語Cの次期仕様C2xでは 属性(attribute) 構文が標準化される。属性構文を先行導入したC++言語とほぼ等価であり、連続するブラケット[[]]を用いる。*1

// C2x
[[nodiscard]] int f();

void g([[maybe_unsed]] int a) {
  [[maybe_unused] int x;
  /* ... */
}

struct [[deprecated("old ver.")]] S {
  /*...*/
};

int n = f();
switch (n) {
case 0:
case 1:
  f();
  [[fallthrough]];
default;
  g(n);
  break;
}

属性指定を行える箇所:

  • 構造体(struct)/共用体(union)の型宣言
  • 列挙型(enum)の型宣言
  • 列挙子(enumerator)の定義
  • 変数/関数仮引数の宣言
  • 関数の宣言/定義
  • 任意の文(statement)

2020年2月時点のC2xドラフト*2で追加される属性は下記4種類*3

  • nodiscard(→(PDF) N2267
  • maybe_unsed(→(PDF) N2270
  • deprecated, deprecated("string-literal")(→(PDF) N2334
  • fallthrough(→(PDF) N2408

C++属性構文との共通点/差分:

  • C++同様に、処理系定義(implementation-defined)の属性を許容する。未知の属性は無視される。
  • C++同様に、処理系定義の属性用に名前空間(相当)を指定できる。*4
  • C2xの属性attrは前後にアンダースコア2つを追加した__attr__と同義。例:[[nodiscard]][[__nodiscard__]]は同じ属性を表す。

関連URL

*1:C2x/C++いずれの言語仕様でも新規トークン [[ や ]] は導入せず、“ブラケット [ または ] が2トークン連続(空白を挟んでもよい)” を属性の開始/終了として扱う。

*2:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2478.pdf

*3:いずれもプログラム意味論には影響を与えず、標準属性を無視してもプログラム動作は変化しない。それぞれC2x標準属性のサポートは処理系定義のオプションとされるが、各処理系は標準属性を実装することが推奨(recommended)される。

*4:C言語には名前空間(namespace)が存在しないため、属性構文でのみ利用する専用の構文要素 attribute-prefix とコロン2つからなる新しいトークンを導入している。┐(´-`)┌

コンストラクタ/デストラクタ×仮想関数呼び出し

C++においてコンストラクタ/デストラクタからの仮想関数呼び出しそれ自体はwell-definedだが、おそらくC++プログラマの期待する振る舞いではない。

バグの温床になりえるため、大抵のコーディング規約で禁止している(はず)。かつてClangに本件を検知する警告オプションが提案*1されたが放棄された模様。

struct Base {
  Base() {
    // コンストラクタ中での仮想関数呼び出しは
    // B::vfを呼び出す(D::vfではない!)
    vf();  
  }
  void g() {
    // 通常(非static)メンバ関数中からであれば
    // オーバーライドされたD::vfを呼び出す
    vf();
  }
  virtual void vf() { /*...*/ }
};

struct Derived : Base {
  virtual void vf() override { /*...*/ }
};

Derived obj;
obj.g();

純粋仮想関数(pure virtual function)が呼び出される場合は未定義動作(undefined behavior)を引き起こす。*2

struct Interface {
  Interface() {
    vf();  // NG: 純粋仮想関数の呼び出し!
  }
  virtual void vf() = 0;
};

struct Concrete : Interface {
  virtual void vf() override { /*...*/ }
};

Concrete obj;

プログラムのバグであることが自明なため、GCCやClangはコンパイル時に警告として報告する。MSVCの場合はプログラム実行時エラー。

  • GCC:pure virtual 'virtual void Interface::vf()' called from constructor
  • Clang:warning: call to pure virtual member function 'vf' has undefined behavior; overrides of 'vf' in subclasses are not available in the constructor of 'Interface'

C++17 15.7/p3より引用(下線部は強調)。C++11/14では12.7/p4。

Member functions, including virtual functions (13.3), can be called during construction or destruction (15.6.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class's non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor's or destructor's class and not one overriding it in a more-derived class. If the virtual function call uses an explicit class member access (8.2.5) and the object expression refers to the complete object of x or one of that object's base class subobjects but not x or one of its base class subobjects, the behavior is undefined. [Example:

struct V {
  virtual void f();
  virtual void g();
};

struct A : virtual V {
  virtual void f();
};

struct B : virtual V {
  virtual void g();
  B(V*, A*);
};

struct D : A, B {
  virtual void f();
  virtual void g();
  D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
  f();     // calls V::f, not A::f
  g();     // calls B::g, not D::g
  v->g();  // v is base of B, the call is well-defined, calls B::g
  a->f();  // undefined behavior, a's type not a base of B
}

-- end example]

関連URL

*1:https://reviews.llvm.org/D56366 New warning call-to-virtual-from-ctor-dtor when calling a virtual function from a constructor or a destructor

*2:C++17 13.4/p6: "Member functions can be called from a constructor (or destructor) of an abstract class; the effect of making a virtual call to a pure virtual function directly or indirectly for the object being created (or destroyed) from such a constructor (or destructor) is undefined."