yohhoyの日記

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

decltype(e)とdecltype((e))のキモチ

プログラミング言語C++における decltype指定子(decltype-specifier) の振る舞いについてメモ。

int x = 42;

// 括弧なしの変数名 x
decltype( x ) y = x;  // int 型
// 括弧付きの式 (x)
decltype((x)) z = x;  // int& 型

一見すると奇妙に思えるdecltype(e)型導出ルールは、「式eの型宣言を探す」という指針に基づいている。

  • 変数名xソースコード上に直接登場する変数x宣言時の型intとなる。
  • (x):式(x)ソースコード上の宣言には現れない。式(x)の左辺値としての性質(lvalueness)を保持する参照型int&となる。*1

C++11言語仕様へのdecltype導入当時の提案文書(PDF) N2115 Decltype (revision 6): proposed wording より一部引用。*2

2.2 Semantics of decltype
Determining the type decltype(e) build on a single guiding principle: look for the declared type of the expression e. If e is a variable or formal parameter, or a function/operator invocation, the programmer can trace down the variable's, parameter's, or function's declaration, and find the type declared for the particular entity directly from the program text. This type is the result of decltype. For expressions that do not have a declaration in the program text, such as literals and calls to built-in operators, lvalueness implies a reference type.

おまけ:括弧付きの変数名のみからなる式(e)の扱いは検討当時も紆余曲折あったようで、(PDF)N1705(rev.4)時点ではdecltype((e))decltype(e)は等価とされていたが、その後のN2115(rev.6)にて現行仕様へと再変更されている。

C++17 10.1.7.2/p4より引用(下線部は強調)。*3

For an expression e, the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding (11.5), decltype(e) is the referenced type as given in the specification of the structured binding declaration;
  • otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access (8.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;
  • otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;
  • otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
  • otherwise, decltype(e) is the type of e.

The operand of the decltype specifier is an unevaluated operand (Clause 8).
[Example:

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 17;   // type is const int&&
decltype(i) x2;            // type is int
decltype(a->x) x3;         // type is double
decltype((a->x)) x4 = x3;  // type is const double&

-- end example] [Note: The rules for determining types involving decltype(auto) are specified in 10.1.7.4. --end note]

関連URL

*1:C++文法上は変数宣言時に括弧を記述できるが(例:int (y) = 42;)、この冗長な括弧は単に無視されてdecltypeの振る舞いには影響を与えない。https://gist.github.com/yohhoy/7b63eafcc42e078f34294857698adfd9

*2:C++仕様には後続文書(PDF)N2343採択されている。

*3:1番目のBulletは 構造化束縛(structured binding) に対するルール。構造化束縛で導入される名前の型導出は、通常の変数宣言よりも複雑な規則となっている。

C++コンセプトとド・モルガンの法則

C++2a(C++20)コンセプトでは制約式(constraint-expression)に論理積(&&)/論理和(||)/論理否定(!)を表現できるが、制約式を用いたオーバーロード解決では通常の論理演算で期待される ド・モルガンの法則(De Morgan's laws) は適用されない。*1

超要約:否定演算!を使ったコンセプトには要注意!

// C++2a
#include <concepts>  // integral, same_asコンセプト

template <typename T>
  requires (!std::integral<T>)
void f(T) { /*...*/ }  // #1

template <typename T>
  requires (!std::integral<T> && std::same_as<T, float>)
void f(T) { /*...*/ }  // #2

f(3.14 );  // OK: #1を選択(T=double)
f(3.14f);  // NG: オーバーロード解決が曖昧(T=float)

2番目の関数呼び出し(T=float)では関数fの#1, #2両オーバーロードとも制約式を満たすが、包摂関係(subsumption relation)は成り立たないためオーバーロード解決が曖昧となる。これは#1, #2の制約式に含まれる部分式!std::integral<T>が、それぞれ異なる原始制約(atomic constraint)と解釈されるため。(「・ω・)「

後述例のように、コンセプトnot_integralを介して “同一の式から構成される原始制約” とすれば期待通り動作する。ここでは#2の制約式not_integral<T> && std::same_as<T, float>は#1の制約式not_integral<T>を包摂(subsume)している。(」・ω・)」

// C++2a
#include <concepts>

template <typename T>
concept not_integral = !std::integral<T>;

template <typename T>
  requires not_integral<T>
void f(T) { /*...*/ }  // #1

template <typename T>
  requires not_integral<T> && std::same_as<T, float>
void f(T) { /*...*/ }  // #2

f(3.14 );  // OK: #1を選択(T=double)
f(3.14f);  // OK: #2を選択(T=float)

C++2a DIS n4861 13.5.1.1/p5より引用(下線部は強調)。注:13.7.6.1=Function template overloading, 13.5.4=Partial ordering by constraints

[Note: A logical negation expression (7.6.2.1) is an atomic constraint; the negation operator is not treated as a logical operation on constraints. As a result, distinct negation constraint-expressions that are equivalent under 13.7.6.1 do not subsume one another under 13.5.4. Furthermore, if substitution to determine whether an atomic constraint is satisfied (13.5.1.2) encounters a substitution failure, the constraint is not satisfied, regardless of the presence of a negation operator. [Example:

template <class T> concept sad = false;
template <class T> int f1(T) requires (!sad<T>);
template <class T> int f1(T) requires (!sad<T>) && true;
int i1 = f1(42);  // ambiguous, !sad<T> atomic constraint expressions (13.5.1.2)
                  // are not formed from the same expression

template <class T> concept not_sad = !sad<T>;
template <class T> int f2(T) requires not_sad<T>;
template <class T> int f2(T) requires not_sad<T> && true;
int i2 = f2(42);  // OK, !sad<T> atomic constraint expressions both come from not_sad

template <class T> int f3(T) requires (!sad<typename T::type>);
int i3 = f3(42);  // error: associated constraints not satisfied due to substitution failure

template <class T> concept sad_nested_type = sad<typename T::type>;
template <class T> int f4(T) requires (!sad_nested_type<T>);
int i4 = f4(42);  // OK, substitution failure contained within sad_nested_type

Here, requires (!sad<typename T::type>) requires that there is a nested type that is not sad, whereas requires (!sad_nested_type<T>) requires that there is no sad nested type. --end example] --end note]

関連URL

リテラル0との比較のみ許容する型

リテラル0との比較のみ許容する型の作り方。そのような型として、C++2a(C++20)三方比較演算子<=>の戻り値型(partial_ordering/ weak_ordering/strong_ordering)がある。

// C++2a
#include <cassert>
#include <compare>

int main()
{
  std::strong_ordering r = (108 <=> 42);
  assert(r > 0);  // OK
  assert(r > 1);  // NG: コンパイルエラー
}

リテラル0がstd::nullptr_t型の値(nullptr)に暗黙変換されることを利用する。

N4861 17.11.2.1/p1, p3より一部引用。

1 The types partial_ordering, weak_ordering, and strong_ordering are collectively termed the comparison category types. (snip)

3 The relational and equality operators for the comparison category types are specified with an anonymous parameter of unspecified type. This type shall be selected by the implementation such that these parameters can accept literal 0 as a corresponding argument. [Example: nullptr_t meets this requirement. --end example] In this context, the behavior of a program that supplies an argument other than a literal 0 is undefined.

関連URL

requires式から利用可能な宣言

C++2a(C++20) コンセプト requires式(requires-expression) では、同式を包含するコンテキストのあらゆる宣言を利用できる。

下記コードのrequires式からは関数テンプレートの仮引数xを参照している。requires式の本体(requirement-body)は評価されず(unevaluated)、型情報(型T)のみを利用している。

#include <iostream>
#include <string>
using namespace std::string_literals;

template <typename T>
T doubling(T x)
{
  if constexpr (requires { x.doubling(); }) {
    x.doubling();  // #1
    return x;
  }
  else if constexpr (requires { x * 2; }) {
    return x * 2;  // #2
  }
  else {
    return x + x;  // #3
  }
}

// doublingメンバ関数を持つクラス型
struct X {
  unsigned m_;
  void doubling()
    { m_ <<= 1; }
  friend std::ostream& operator<<(std::ostream& os, const X& x)
    { return os << "X{" << x.m_ << "}"; }
};

std::cout << doubling(3.14);    // 6.28   (#2)
std::cout << doubling("abc"s);  // abcabc (#3)
std::cout << doubling(X{21});   // X{42}  (#1)

1番目constexpr if文の条件式記述では下記の選択肢が考えられる:

  • requires { x.doubling(); }:関数パラメータ宣言を利用
  • requires (T t) { t.doubling(); }:ローカルパラメータ導入と利用
  • requires { T{}.doubling(); }:テンプレートパラメータを利用

N4861 7.5.7/p1-2, 4-5より一部引用(下線部は強調)。

1 A requires-expression provides a concise way to express requirements on template arguments that can be checked by name lookup (6.5) or by checking properties of types and expressions.
 requires-expression:
   requires requirement-parameter-listopt requirement-body
 requirement-parameter-list:
   ( parameter-declaration-clauseopt )
 requirement-body:
   { requirement-seq }
 (snip)
2 A requires-expression is a prvalue of type bool whose value is described below. Expressions appearing within a requirement-body are unevaluated operands (7.2).

4 A requires-expression may introduce local parameters using a parameter-declaration-clause (9.3.3.5). (snip)
5 The requirement-body contains a sequence of requirements. These requirements may refer to local parameters, template parameters, and any other declarations visible from the enclosing context.

関連URL:

ラムダ式のオーバーロード

プログラミング言語C++において、ラムダ式オーバーロード(もどき)を実装する方法。

// C++17
template <typename... Ts>
struct overloaded : Ts... {
  // 基底クラス(ラムダ式のクロージャクラス)が提供する
  // operator()群をoverloadedクラス自身から公開する
  using Ts::operator()...;
};

// テンプレート引数Ts...の推論ガイド宣言
template <typename... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

// 文字列(const char*), doube型, それ以外でオーバーロード
auto printer = overloaded{
  [](const char* s) { std::cout << std::quoted(s) << '\n'; },
  [](double v) { std::cout << std::fixed << v << '\n'; },
  [](auto x) { std::cout << x << '\n'; }
};

printer("Hi");  // "Hi"
printer(3.14);  // 3.140000
printer(42);    // 42

C++17現在、集成体(aggregate)に対しては推論ガイド(deduction guide)の宣言が必要となる。(PDF)P1816R0採択によりC++2a(C++20)から推論ガイドが暗黙に生成されるようになる(C++17同様に明示宣言してもよい)。

// C++2a
template <typename... Ts>
struct overloaded : Ts... {
  using Ts::operator()...;
};
// 下記の推論ガイドが暗黙宣言される
// template <typename... Ts> overloaded(Ts...) -> overloaded<Ts...>;

関連URL

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() noexcept { return std::suspend_always{}; }
    auto final_suspend() noexcept { 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() { throw; }
  };
  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;
  }
}

2022-01-13追記:co_awaitco_yieldでは演算子の優先順位が異なるため*1、いつでも両者を相互変換できるわけではない。co_awaitは単項演算子と同じく優先順位が高く、co_yieldは代入演算子と同じく優先順位が低い。*2

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