yohhoyの日記

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

decltype(auto) as non-type template-parameter

非型テンプレートのプレースホルダ型にはautoまたはdecltype(auto)いずれも利用できる。素直にautoを使うべき。

本記事の内容はStackOverflowで見つけた質問と回答に基づく。

template <auto N>
struct S { /*...*/ };
// または
template <decltype(auto) N>
struct S { /*...*/ };

S<42> obj;

C++17 10.1.7.4/p1, p5, 17.1/p4より一部引用(下線部は強調)。

1 The auto and decltype(auto) type-specifiers are used to designate a placeholder type that will be replaced later by deduction from an initializer. The auto type-specifier is also used to introduce a function type having a trailing-return-type or to signify that a lambda is a generic lambda (8.1.5.1). The auto type-specifier is also used to introduce a structured binding declaration (11.5).

5 A placeholder type can also be used in the type-specifier-seq in the new-type-id or type-id of a new-expression (8.3.4) and as a decl-specifier of the parameter-declaration's decl-specifier-seq in a template-parameter (17.1).

4 A non-type template-parameter shall have one of the following (optionally cv-qualified) types:

  • (snip)
  • a type that contains a placeholder type (10.1.7.4).

関連URL

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