yohhoyの日記

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

単一メンバunionの使い道

プログラミング言語C++において、単一メンバしか含まない共用体(union)を用いるとオブジェクトの明示的な生成/破棄操作が可能となる。貧者(poor man's)のOptional。

#include <iostream>

template <typename T>
union Wrapper {
  // 共用体のコンストラクタ/デストラクタ定義は必須
  Wrapper() {}
  ~Wrapper() {}
  // 明示的なオブジェクト初期化
  void init() { new (&obj_) Holder; }
  // 明示的なオブジェクト廃棄
  void destroy() { obj_.~Holder(); }
  // 制御対象クラス
  struct Holder {
    T m_;
  } obj_;
};

struct S {
  S() { std::cout << "S::ctor\n"; }
  ~S() { std::cout << "S::dtor\n"; }
};

int main()
{
  Wrapper<S> opt;
  // このタイミングではS型オブジェクトは生成されない

  std::cout << "call init\n";
  opt.init();  // S::S()を呼び出し

  std::cout << "call destroy\n";
  opt.destroy();  // S::~S()を呼び出し
}

C++17 12.3/p1, 6より一部引用。

1 In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended (6.8). At most one of the non-static data members of an object of union type can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time. (snip)

6 [Note: In general, one must use explicit destructor calls and placement new-expression to change the active member of a union. -- end note] [Example: Consider an object u of a union type U having non-static data members m of type M and n of type N. If M has a non-trivial destructor and N has a non-trivial constructor (for instance, if they declare or inherit virtual functions), the active member of u can be safely switched from m to n using the destructor and placement new-expression as follows:

u.m.~M();
new (&u.n) N;

-- end example]

関連URL

名前によるUnicodeリテラル表現

PythonではUnicodeコードポイントによるリテラル表現*1の他に、Unicode文字データベース(UCD; Unicode Character Database)*2に準じた名前表現もサポートする。

print("\U0001F4DB")      # 📛
print("\N{NAME BADGE}")  # 📛

print("\N{TOFU ON FIRE}")
# SyntaxError:
#   (unicode error) 'unicodeescape' codec can't decode bytes in position 0-15:
#   unknown Unicode character name

関連URL

*1:¥uXXXXUCS-2) または ¥UXXXXXXXX(UCS-4)

*2:https://www.unicode.org/reports/tr44/

Go言語の++/--は文(Statement)

Go言語におけるインクリメント++/デクリメント--演算子は、後置(postfix)記法のみが許容され、式(expression)ではなく 文(statement) を構成する。

i++;  // i += 1; と等価
i--;  // i -= 1; と等価

Why are ++ and -- statements and not expressions? And why postfix, not prefix?
Without pointer arithmetic, the convenience value of pre- and postfix increment operators drops. By removing them from the expression hierarchy altogether, expression syntax is simplified and the messy issues around order of evaluation of ++ and -- (consider f(i++) and p[i] = q[++i]) are eliminated as well. The simplification is significant. As for postfix vs. prefix, either would work fine but the postfix version is more traditional; insistence on prefix arose with the STL, a library for a language whose name contains, ironically, a postfix increment.

https://golang.org/doc/faq

関連URL

独自診断メッセージ diagnose_if属性

Clangコンパイラはユーザ定義のコンパイル警告/エラーメッセージ出力を行うdiagnose_if属性を提供する。

The diagnose_if attribute can be placed on function declarations to emit warnings or errors at compile-time if calls to the attributed function meet certain user-defined criteria. For example:

void abs(int a)
  __attribute__((diagnose_if(a >= 0, "Redundant abs call", "warning")));
void must_abs(int a)
  __attribute__((diagnose_if(a >= 0, "Redundant abs call", "error")));

int val = abs(1); // warning: Redundant abs call
int val2 = must_abs(1); // error: Redundant abs call
int val3 = abs(val);
int val4 = must_abs(val); // Because run-time checks are not emitted for
                          // diagnose_if attributes, this executes without
                          // issue.
https://releases.llvm.org/5.0.0/tools/clang/docs/AttributeReference.html#diagnose-if

C++標準ライブラリlibcxxでは、このdiagnose_if属性を利用してC++標準ライブラリ要件(requirements)診断を部分的に行っている。

電子書籍"Modern C (2nd Ed.)"

最新C17までカバーした、モダンなC言語プログラミングに関する電子書籍。PDF形式(315頁)はCC BY-NC-ND 4.0ライセンス。

イテレータに->演算子オーバーロードは必要?

C++標準ライブラリが定める InputIterator 要件(requirement) と input_iteratorコンセプト(concept) の変遷についてメモ。

まとめ:

N4835(C++2a WD)

Cpp17InputIterator要件はoperator->オーバーロードを要求するが、input_iteratorコンセプトはoperator->オーバーロードを要求しない。
C++2aに向けて採択された提案文書(PDF)P0896R4では、[iterator.requirements.general]セクションから下記Wordingが明示的に削除されている。

An iterator i for which the expression (*i).m is well-defined supports the expression i->m with the same semantics as (*i).m.

N4835*1 23.2, 23.3.1/p1, 23.3.4.2/p1-2, 23.3.4.6/p1, 23.3.4.9/p1, 23.3.5.2 Table 83より一部引用。

Header <iterator> synopsis

namespace std {
  template<class T> using with-reference = T&;  // exposition only
  template<class T> concept can-reference       // exposition only
    = requires { typename with-reference<T>; };
  template<class T> concept dereferenceable     // exposition only
    = requires(T& t) {
      { *t } -> can-reference;  // not required to be equality-preserving
    };
  // (snip)
}

1 Iterators are a generalization of pointers that allow a C++ program to work with different data structures (for example, containers and ranges) in a uniform manner. To be able to construct template algorithms that work correctly and efficiently on different types of data structures, the library formalizes not just the interfaces but also the semantics and complexity assumptions of iterators. An input iterator i supports the expression *i, resulting in a value of some object type T, called the value type of the iterator. (snip)

1 Types that are readable by applying operator* model the readable concept, including pointers, smart pointers, and iterators.

template<class In>
  concept readable =
    /*(snip)*/;

2 Given a value i of type I, I models readable only if the expression *i is equality-preserving. [Note: The expression *i is indirectly required to be valid via the exposition-only dereferenceable concept (23.2). -- end note]

1 The input_or_output_iterator concept forms the basis of the iterator concept taxonomy; every iterator models input_or_output_iterator. This concept specifies operations for dereferencing and incrementing an iterator. Most algorithms will require additional operations to compare iterators with sentinels (23.3.4.7), to read (23.3.4.9) or write (23.3.4.10) values, or to provide a richer set of iterator movements (23.3.4.11, 23.3.4.12, 23.3.4.13).

template<class I>
  concept input_or_output_iterator =
    requires(I i) {
      { *i } -> can-reference;
    } &&
    weakly_incrementable<I>;

1 The input_iterator concept defines requirements for a type whose referenced values can be read (from the requirement for readable (23.3.4.2)) and which can be both pre- and post-incremented. [Note: Unlike the Cpp17InputIterator requirements (23.3.5.2), the input_iterator concept does not need equality comparison since iterators are typically compared to sentinels. -- end note]

template<class I>
  concept input_iterator =
    input_or_output_iterator<I> &&
    readable<I> &&
    requires { typename ITER_CONCEPT(I); } &&
    derived_from<ITER_CONCEPT(I), input_iterator_tag>;

Table 83: Cpp17InputIterator requirements (in addition to Cpp17Iterator)

Expression
a->m
Operational semantics
(*a).m
Assertion/note pre-/post-condition
Expects: a is dereferenceable.

C++17

Input iterator要件はoperator->オーバーロードを要求する。 C++17 27.2.3 Table 95より該当箇所を引用。

Expression
a->m
Operational semantics
(*a).m
Assertion/note pre-/post-condition
Requires: a is dereferenceable.

C++17以降、LWG DR 2790にて istreambuf_iterator::operator-> は削除された。

C++11/14

Input iterator要件はoperator->オーバーロードを要求する。 C++11 24.2.3 Table 107より該当箇所を引用。C++14でも同一。

Expression
a->m
Operational semantics
(*a).m
Assertion/note pre-/post-condition
pre: a is dereferenceable.

C++11以降、LWG DR 659にて istreambuf_iterator::operator-> が追加された。

C++03

Input iterator要件はoperator->オーバーロードを要求する。 C++03 24.1.1 Table 72より該当箇所を引用。

operation
a->m
semantics, pre/post-condition
pre: (*a).m is well-defined
Equivalent to (*a).m

C++03時点では、istreambuf_iterator::operator->は提供されない。


関連URL

same_asコンセプトとSymmetric Subsumption Idiom

C++2a(C++20)ライブラリ提供の標準コンセプトstd::same_as、およびコンセプト定義における対称包摂イディオム(Symmetric Subsumption Idiom)についてメモ。

制約式std::same_as<X, Y>と制約式std::same_as<Y, X>は対称関係、つまり互いに一方が他方を包摂する(subsume)関係にある。これによりコンセプトへのテンプレートパラメータ指定順が一致していなくとも、制約式を用いた関数オーバーロード解決の半順序関係を表現できる。

// C++2a(C++20)
#include <concepts>

template <typename X, typename Y>
  requires std::same_as<X, Y>
constexpr int f() { return 1; }  // #1

template <typename X, typename Y>
  requires std::same_as<Y, X> && (1 < sizeof(X))
constexpr int f() { return 2; }  // #2

f<int, int>();  // OK: #2を呼び出す

もしsame_asコンセプトがこのような対称性をもたない場合、関数オーバーロード解決は曖昧になり上記コードはill-formedとなってしまう。*1

C++2a標準ライブラリ

標準コンセプトsame_as<T,U>の定義では動作説明用(exposition only)のコンセプトsame-as-implを用いて、型パラメータT, Uに対象関係が成り立つようにしている。ここではC++2a言語仕様上、メタ関数is_same_vの直接記述ではなくコンセプトsame-as-impl経由が必須となる。(詳細後述)
N4830*2 14.8.2より引用。

template<class T, class U>
  concept same-as-impl = is_same_v<T, U>;  // exposition only

template<class T, class U>
  concept same_as = same-as-impl<T, U> && same-as-impl<U, T>;

1 [Note: same_as<T, U> subsumes same_as<U, T> and vice versa. -- end note]

当初はナイーブな定義とNoteによる動作説明になっており、パラメータの対称性を実現にはコンパイラマジックが必要であった。LWG3182によりライブラリ定義のみで対称性が実現されるよう修正された経緯がある(下線部は強調)。*3

The specification of the Same concept in 18.4.2 [concept.same]:

template<class T, class U>
  concept Same = is_same_v<T, U>;

-1- Same<T, U> subsumes Same<U, T> and vice versa.

seems contradictory. From the concept definition alone, it is not the case that Same<T, U> subsumes Same<U, T> nor vice versa. Paragraph 1 is trying to tell us that there's some magic that provides the stated subsumption relationship, but to a casual reader it appears to be a misannotated note. We should either add a note to explain what's actually happening here, or define the concept in such a way that it naturally provides the specified subsumption relationship.

Given that there's a straightforward library implementation of the symmetric subsumption idiom, the latter option seems preferable.

コンセプト利用

コンセプトsame_as定義にコンセプトsame-as-implを利用するとき、2つの制約式[A]same_as<X, Y>, [B]same_as<Y, X>の関係は次のように説明できる。

template<class T, class U>
  concept same-as-impl = is_same_v<T, U>/*#0*/;

template<class T, class U>
  concept same_as = same-as-impl<T, U> && same-as-impl<U, T>;

それぞれの制約式を正規化(normalization)すると、原子制約(atomic constraint)導出時のパラメータマッピング(parameter mapping)を ↦ 表記して、次の正規形(normal form)が得られる:

  • [A]same_as<X, Y>の正規形は is_same_v<T↦X, U↦Y>#0is_same_v<T↦Y, U↦X>#0
  • [B]same_as<Y, X>の正規形は is_same_v<T↦Y, U↦X>#0is_same_v<T↦X, U↦Y>#0

制約P=[A]same_as<X, Y>, 制約Q=[B]same_as<Y, X>とおくと、PのDNF(選言標準形; disjunctive normal form)およびQのCNF(連言標準形; conjunctive normal form)の各項は次の通り:

  • P1=(is_same_v<T↦X, U↦Y>#0is_same_v<T↦Y, U↦X>#0)
  • Q1=is_same_v<T↦Y, U↦X>#0, Q2=is_same_v<T↦X, U↦Y>#0

項P1は2つの原子制約 P1,1=is_same_v<T↦X, U↦Y>#0, P1,2=is_same_v<T↦Y, U↦X>#0からなり、QのCNF項に含まれる原始制約Qj,bに対して同一(identical; ≡ 表記)な原子制約Pi,aが存在する:

  • 原子制約Q1,1=is_same_v<T↦Y, U↦X>#0 ≡ 原子制約P1,2=is_same_v<T↦Y, U↦X>#0 つまり “項P1 subsumes 項Q1
  • 原子制約Q2,1=is_same_v<T↦X, U↦Y>#0 ≡ 原子制約P1,1=is_same_v<T↦X, U↦Y>#0 つまり “項P1 subsumes 項Q2

以上より、制約Pの全てのDNF項(P1)は制約Qの全てのCNF項(Q1, Q2)を包摂する、つまり “[A]same_as<X, Y> subsumes [B]same_as<Y, X>” が導出される。

制約P, Qを入れ替えると、同様に対称な関係 “[B]same_as<Y, X> subsumes [A]same_as<X, Y>” が導出される。

メタ関数利用

コンセプトsame_as定義にメタ関数is_same_vを直接利用したとき、2つの制約式[A]same_as<X, Y>, [B]same_as<Y, X>の関係は次のように説明できる。

template<class T, class U>
  concept same_as = is_same_v<T, U>/*#1*/ && is_same_v<U, T>/*#2*/;

それぞれの制約式を正規化すると、次の正規形が得られる:

  • [A]same_as<X, Y>の正規形は is_same_v<T↦X, U↦Y>#1is_same_v<U↦Y, T↦X>#2
  • [B]same_as<Y, X>の正規形は is_same_v<T↦Y, U↦X>#1is_same_v<U↦X, T↦Y>#2

制約P=[A]same_as<X, Y>, 制約Q=[B]same_as<Y, X>とおくと、PのDNFおよびQのCNFの各項は次の通り:

  • P1=(is_same_v<T↦X, U↦Y>#1is_same_v<U↦Y, T↦X>#2)
  • Q1=is_same_v<T↦Y, U↦X>#1, Q2=is_same_v<U↦X, T↦Y>#2

項P1は2つの原子制約 P1,1=is_same_v<T↦X, U↦Y>#1, P1,2=is_same_v<U↦Y, T↦X>#2からなるが、QのCNF項に含まれる原始制約Q1,1やQ2,1と “同じ式(the same expression)” かつ “同等のパラメータマッピング” をもつ原子制約が無い、つまりいずれの原子制約Qj,bに対しても同一(identical)な原子制約Pi,aは存在しない。*4

つまり[A]same_as<X, Y>は[B]same_as<Y, X>を包摂しない。制約P, Qを入れ替えても同様に包摂関係は成り立たない。


関連URL

*1:C++言語仕様を厳格に解釈すると 1 == sizeof(int) となる処理系が存在しうるため、該当コードでオーバーロード#1が選択される可能性もある。本記事ではそのような処理系の存在は無視する。

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

*3:N4820以前のコンセプト名は Same とされていたが、(PDF)P1754R1採択によって same_as へと改名された。詳細はC++標準コンセプトの名前付けガイドラインを参照。

*4:N4830 13.4.1.2/p2: "Two atomic constraints are identical if they are formed from the same expression and the targets of the parameter mappings are equivalent according to the rules for expressions described in 13.6.6.1."