yohhoyの日記

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

C++標準コンセプトの名前付けガイドライン

C++2a(C++20)標準ライブラリに導入される コンセプト(concept) の名前付けガイドラインについて。

2019年Cologne会合にて (PDF)P1754R1 が採択され、Ranges TS提案当初から PascalCase 形式で検討されていた命名規則から snake_case 形式へと変更された。これにより従来C++標準ライブラリとの一貫性は向上したが、その名前のみからはコンセプトなのかクラスやメタ関数*1なのかを判別しづらくなっている。*2

提案文書P1851R0では、P1754R1から改善した下記ガイドラインを提案している。

  • コンセプト名は snake_case 形式とする。
  • コンセプトを示す接頭辞(prefix)/接尾辞(suffix)は使わない。
  • コンセプトの目的に応じた3つのカテゴリ:特性(Capabilities)、抽象(Abstrations)、その他の述語(predicate)
  • 特性コンセプト(capability concept):ある関数/メンバ関数呼び出しが可能といった単一要件で構成される。
    • 特性コンセプトの名前は、その要件を説明する形容詞(adjective)とする。
    • 要求する特性が関数のとき、そのコンセプト名には関数名に-ible-able接尾辞をつけたものとする。例:swap可能を要求するswappable, コピー構築可能を要求するcopy_constructible
  • 抽象コンセプト(abstract concept):複数要件から構成されたりコンセプト構成のルートにあたるハイレベル・コンセプト。
    • 抽象コンセプトの名前は、新しい専門用語(terminology)を導入する名詞(noun)とする。例:forward_iterator(要件を列挙しただけのhas_increment_equality_and_dereferenceはNG)
    • 抽象コンセプトの名前は、同コンセプトを満たす型の名前よりも総称的(general)であること。
  • その他の述語コンセプト:下記に該当する場合は、型トレイツ(type traits)の命名規則に従う。ただしis_接頭辞はつけない。
    • 主に複数の型引数をとり型制約に利用されるときは、適切な前置詞(preposition)で終わる名前とする。例:swappable_with, same_as<int>, constructible_with<args>, sentinel_for<iterator>
    • 主に別コンセプトの定義やrequires節で利用されるときは、前置詞(preposition)は付けない。例:swappable, mergeable

関連URL

*1:標準ヘッダ <type_traits> で提供される、ある型の特性(traits)をコンパイル時に判別するテンプレート関数群。

*2:P1754R1では変更根拠として、PascalCase はドメイン固有のエンティティ名として用いられるケースが多いことと、アプリケーションコードでの using namespace std; 利用によって生じる名前衝突回避を挙げている。

フライングWindows API待機関数

Windows APIタイムアウト指定待機関数では、指定期間よりも僅かに早くタイムアウト発生する。この振る舞いは仕様通り(by design)とのこと。

  • WaitForSingleObject, WaitForSingleObjectEx
  • WaitForMultipleObjects, WaitForMultipleObjectsEx
  • MsgWaitForMultipleObjects, MsgWaitForMultipleObjectsEx
  • etc.

Windows OSのデフォルトタイマ割込間隔は 64 [Hz]=15.625 [msec] となっているため、平均的に数ミリ秒だけ早くタイムアウトが発生しうる。

Wait Functions and Time-out Intervals
The accuracy of the specified time-out interval depends on the resolution of the system clock. The system clock "ticks" at a constant rate. If the time-out interval is less than the resolution of the system clock, the wait may time out in less than the specified length of time. If the time-out interval is greater than one tick but less than two, the wait can be anywhere between one and two ticks, and so on.

https://docs.microsoft.com/windows/win32/sync/wait-functions

関連URL

nan("is Not-a-Number")

プログラミング言語C/C++における浮動小数点数 NaN(Not-a-Number)*1 について。

C/C++標準ライブラリはquiet NaN*2を返すnan関数を提供し、同関数では処理系定義(implementation-defined)のタグ文字列を受け取る。一般的には空文字列""を指定するが、ライブラリ仕様上は任意の文字列*3を指定可能。

// C
#include <math.h>
double d1 = nan("42");   // OK
double d2 = nan("wtf");  // OK
// C++
#include <cmath>
double d1 = std::nan("42");   // OK
double d2 = std::nan("wtf");  // OK

IEC 60559浮動小数点型のバイナリ表現は {符号S, 指数部E, 仮数T} の組で表され、指数部Eが全ビット1かつ仮数Tが非ゼロのとき NaN と解釈される。上記で指定するタグは仮数部(significand)ビット列*4に反映される。ただし、ISO C/C++およびIEC 60559いずれの仕様でもその意味を規定しないため、実動作は処理系に依存する。

C11 7.12.11.2/p1-2, 7.22.1.3/p3-4より一部引用(下線部は強調)。C++標準ライブラリに含まれるC標準ライブラリはC言語仕様に従う。*5

#include <math.h>
double nan(const char *tagp);
float nanf(const char *tagp);
long double nanl(const char *tagp);

2 The call nan("n-char-sequence") is equivalent to strtod("NAN(n-char-sequence)", (char**) NULL); the call nan("") is equivalent to strtod("NAN()", (char**) NULL). If tagp does not point to an n-char sequence or an empty string, the call is equivalent to strtod("NAN", (char**) NULL). Calls to nanf and nanl are equivalent to the corresponding calls to strtof and strtold.

3 The expected form of the subject sequence is an optional plus or minus sign, then one of the following:

  • (snip)
  • NAN or NAN(n-char-sequenceopt), ignoring case in the NAN part, where:

  n-char-sequence:
    digit
    nondigit
    n-char-sequence digit
    n-char-sequence nondigit
(snip)

4 (snip) A character sequence NAN or NAN(n-char-sequenceopt) is interpreted as a quiet NaN, if supported in the return type, else like a subject sequence part that does not have the expected form; the meaning of the n-char sequence is implementation-defined.293)(snip)
脚注293) An implementation may use the n-char sequence to determine extra information to be represented in the NaN's significand.

C99 (PDF)Rationale 5.2.4.2.2, 7.20.1.3より一部引用。

5.2.4.2.2 Characteristics of floating types <float.h>
NaNs
The primary utility of quiet NaNs, as stated in IEC 60559, "to handle otherwise intractable situations, such as providing a default value for 0.0/0.0," is supported by C99.

Other applications of NaNs may prove useful. Available parts of NaNs have been used to encode auxiliary information, for example about the NaN’s origin. Signaling NaNs might be candidates for filling uninitialized storage; and their available parts could distinguish uninitialized floating objects. IEC 60559 signaling NaNs and trap handlers potentially provide hooks for maintaining diagnostic information or for implementing special arithmetics.

However, C support for signaling NaNs, or for auxiliary information that could be encoded in NaNs, is problematic. Trap handling varies widely among implementations. Implementation mechanisms may trigger signaling NaNs, or fail to, in mysterious ways. The IEC 60559 floating-point standard recommends that NaNs propagate; but it does not require this and not all implementations do. And the floating-point standard fails to specify the contents of NaNs through format conversion. Making signaling NaNs predictable imposes optimization restrictions that anticipated benefits don’t justify. For these reasons this standard does not define the behavior of signaling NaNs nor specify the interpretation of NaN significands.

7.20.1.3 The strtod, strtof, and strtold functions
So much regarding NaN significands is unspecified because so little is portable. Attaching meaning to NaN significands is problematic, even for one implementation, even an IEC 60559 one. For example, the IEC 60559 floating-point standard does not specify the effect of format conversions on NaN significands. Conversions, perhaps generated by the compiler, may alter NaN significands in obscure ways.

IEEE 754 2.1, 6.2, 6.2.1より一部引用。

2.1 Definitions
payload: The diagnostic information contained in a NaN, encoded in part of its trailing significand field.

6.2 Operations with NaNs
Two different kinds of NaN, signaling and quiet, shall be supported in all floating-point operations. Signaling NaNs afford representations for uninitialized variables and arithmetic-like enhancements (such as complex-affine infinities or extremely wide range) that are not in the scope of this standard. Quiet NaNs should, by means left to the implementer's discretion, afford retrospective diagnostic information inherited from invalid or unavailable data and results. To facilitate propagation of diagnostic information contained in NaNs, as much of that information as possible should be preserved in NaN results of operations.

6.2.1 NaN encodings in binary formats
All binary NaN bit strings have all the bits of the biased exponent field E set to 1 (see 3.4). A quiet NaN bit string should be encoded with the first bit (d1) of the trailing significand field T being 1. A signaling NaN bit string should be encoded with the first bit of the trailing significand field being 0. If the first bit of the trailing significand field is 0, some other bit of the trailing significand field must be non-zero to distinguish the NaN from infinity. In the preferred encoding just described, a signaling NaN shall be quieted by setting d1 to 1, leaving the remaining bits of T unchanged.

For binary formats, the payload is encoded in the p-2 least significant bits of the trailing significand field.

関連URL

*1:https://ja.wikipedia.org/wiki/NaN

*2:C/C++言語仕様およびIEC 60559仕様(旧IEC 559)では、2種類のNaN "quiet NaN" と "signaling NaN" を規定している。

*3:厳密には アンダースコア(_)、アルファベット(a-z A-Z)、数値(0-9)の組み合わせが許容される。(C11 6.4.2.1/p1)

*4:仮数部(significand) MSB 2ビットはquiet NaN/signaling Nanの区別に利用するため、以降のビット列がペイロード(payload)となる。

*5:C++17 20.2/p2: "The descriptions of many library functions rely on the C standard library for the semantics of those functions. In some cases, the signatures specified in this International Standard may be different from the signatures in the C standard library, and additional overloads may be declared in this International Standard, but the behavior and the preconditions (including any preconditions implied by the use of an ISO C restrict qualifier) are the same unless otherwise stated."

CUDA同期メモリ転送関数 != 同期動作

CUDAメモリ転送系関数の Async サフィックス有無*1と、実際の同期(synchronous)/非同期(asynchronous)動作は1:1対応しない。Asyncサフィックス無しメモリ転送関数でも、条件によっては非同期動作となる可能性がある

API synchronization behavior
The API provides memcpy/memset functions in both synchronous and asynchronous forms, the latter having an "Async" suffix. This is a misnomer as each function may exhibit synchronous or asynchronous behavior depending on the arguments passed to the function.

https://docs.nvidia.com/cuda/cuda-runtime-api/api-sync-behavior.html

シングルスレッド処理のホストかつ単一デバイス利用という単純構成では、特に意識しなくとも問題にはならない(たぶん)。一方、ホスト側がマルチスレッド処理であったり、複数GPU間でのデバイス間メモリ転送を行う場合には、非同期動作には十分留意する必要がある。罠すぎる(#^ω^)

後者環境では、CUDAストリーム(cudaStream_tCUstream)とAsyncサフィックス付き関数を組み合わせ、明示的な同期関数(cudaStreamSynchronizecuStreamSynchronize)呼び出しによってメモリ転送完了を待機する。*2

Memcpy
In the reference documentation, each memcpy function is categorized as synchronous or asynchronous, corresponding to the definitions below.

Synchronous

  1. All transfers involving Unified Memory regions are fully synchronous with respect to the host.
  2. For transfers from pageable host memory to device memory, a stream sync is performed before the copy is initiated. The function will return once the pageable buffer has been copied to the staging memory for DMA transfer to device memory, but the DMA to final destination may not have completed.
  3. For transfers from pinned host memory to device memory, the function is synchronous with respect to the host.
  4. For transfers from device to either pageable or pinned host memory, the function returns only once the copy has completed.
  5. For transfers from device memory to device memory, no host-side synchronization is performed.
  6. For transfers from any host memory to any host memory, the function is fully synchronous with respect to the host.

Asynchronous

  1. For transfers from device memory to pageable host memory, the function will return only once the copy has completed.
  2. For transfers from any host memory to any host memory, the function is fully synchronous with respect to the host.
  3. For all other transfers, the function is fully asynchronous. If pageable memory must first be staged to pinned memory, this will be handled asynchronously with a worker thread.
https://docs.nvidia.com/cuda/cuda-runtime-api/api-sync-behavior.html

関連URL

*1:例:CUDA Runtime API の cudaMemcpy と cudaMemcpyAsync、CUDA Driver API の cuMemcpy と cuMemcpyAsync など。

*2:厳密には、転送元/先メモリ区分に応じて同期/非同期動作が決定する。現実的には細かいルールを覚えて慎重にコーデイングするより、最初からCUDAストリーム×Asyncサフィックス付き関数を使うのがベターと思う。

Hidden Friends

プログラミング言語C++におけるライブラリ設計で「ADL(Argument Dependent Lookup)経由でのみ呼び出し可能な非メンバ関数演算子オーバーロード 定義」を実現するテクニック。

2019-12-03追記:2019年11月会合にて (PDF)P1965R0 が採択され、C++標準規格上の用語(term)として "hidden friends" への言及が明記された。

下記説明コードのみではメリットがわかりづらいが、非メンバ関数インタフェース追加による名前空間汚染の抑止と、プログラマが意図しない関数呼び出しによるトラブル回避が主目的。

namespace NS1 {
  struct C { /*...*/ };

  // 非メンバ begin/end 関数
  inline C::iterator begin(C& c) { return c.begin(); }
  inline C::iterator end(C& c) { return c.end(); }
}
namespace NS2 {
  struct C {
    /*...*/
    // "Hidden Friends" begin/end 関数
    friend iterator begin(C& c) { return c.begin(); }
    friend iterator end(C& c) { return c.end(); }
  };
}

NS1::C c1;
auto b1 = begin(c1);     // OK: ADL経由でNS1::begin関数を見つける
auto e1 = NS1::end(c1);  // OK: 完全修飾名でNS1::end関数を指定

NS2::C c2;
auto b2 = begin(c2);     // OK: ADL経由でNS2::begin関数を見つける
auto e2 = NS2::end(c2);  // NG: 完全修飾名ではNS2::end関数を呼び出せない

トラブル事例

LWG3065 よりC++標準ライブラリで実際に問題となったコードを引用する。名前空間std::filesystemの取り込み(using)によりoperator==(const path&, const path&) が導入され、左辺ではコンストラクpath(const wchar_t(&)[N])*1が右辺では変換コンストラクpath(string&&)*2が暗黙に呼び出されることで、文字列比較ではなくパス名(pathname)比較 path(L"a//b") == path("a/b") が行われる*3。この問題は Clang 7.0.0 にて再現確認できた。

#include <assert.h>
#include <string>
#include <filesystem>

using namespace std;
using namespace std::filesystem;

int main() {
  bool b = L"a//b" == std::string("a/b");
  assert(b); // passes. What?!
  return b;
}

同種の問題は LWG2989 でも報告、標準ライブラリ修正されている。

提案文書(PDF)P1601R0 Recommendations for Specifying "Hidden Friends"より一部引用(下線部は強調)。

When there is no additional, out-of-class/namespace-scope, declaration of the befriended entity, such an entity has become known as a hidden friend of the class granting friendship. Were there such an out-of-class/namespace-scope declaration, the entity would be no longer hidden, as the second declaration would make the name visible to qualified and to unqualified lookup.


There have been recent discussions about employing this hidden friend technique, where applicable, throughout the standard library so that the declared entities (typically operator functions such as the new spaceship operator) would be found via ADL only. Because the library has not previously deliberately restricted lookup in this way, there is no precedent for specifying such a requirement. The remainder of this paper provides specification guidance to proposal authors who intend to impose such a requirement.

C++17 6.4.2/p4, 10.3.1.2/p3より一部引用(下線部は強調)。

When considering an associated namespace, the lookup is the same as the lookup performed when the associated namespace is used as a qualifier (6.4.3.2) except that:

  • Any using-directives in the associated namespace are ignored.
  • Any namespace-scope friend functions or friend function templates declared in associated classes are visible within their respective namespaces even if they are not visible during an ordinary lookup (14.3).
  • All names except those of (possibly overloaded) functions and function templates are ignored.

If a friend declaration in a non-local class first declares a class, function, class template or function template the friend is a member of the innermost enclosing namespace. The friend declaration does not by itself make the name visible to unqualified lookup (6.4.1) or qualified lookup (6.4.3). [Note: The name of the friend will be visible in its namespace if a matching declaration is provided at namespace scope (either before or after the class definition granting friendship). -- end note] If a friend function or function template is called, its name may be found by the name lookup that considers functions from namespaces and classes associated with the types of the function arguments (6.4.2). If the name in a friend declaration is neither qualified nor a template-id and the declaration is a function or an elaborated-type-specifier, the lookup to determine whether the entity has been previously declared shall not consider any scopes outside the innermost enclosing namespace. [Note: The other forms of friend declarations cannot declare a new member of the innermost enclosing namespace and thus follow the usual lookup rules. -- end note] [Example:

// Assume f and g have not yet been declared.
void h(int);
template <class T> void f2(T);
namespace A {
  class X {
    friend void f(X);         // A::f(X) is a friend
    class Y {
      friend void g();        // A::g is a friend
      friend void h(int);     // A::h is a friend
                              // ::h not considered
      friend void f2<>(int);  // ::f2<>(int) is a friend
    };
  };

  // A::f, A::g and A::h are not visible here
  X x;
  void g() { f(x); }       // definition of A::g
  void f(X) { /*...*/ }    // definition of A::f
  void h(int) { /*...*/ }  // definition of A::h
  // A::f, A::g and A::h are visible here and known to be friends
}

using A::x;

void h() {
  A::f(x);
  A::X::f(x);    // error: f is not a member of A::X
  A::X::Y::g();  // error: g is not a member of A::X::Y
}

-- end example]

関連URL

*1:厳密には template<class Source> path(const Source& source, format fmt = auto_format) テンプレートコンストラクタが選択され(30.10.8.3, 30.10.8.4.1/p7)POSIXベースOSでは未規定(unspecified)なエンコード変換が行われる(30.10.8.2.2)

*2:POSIXベースOSでは path::string_type 型は basic_string<char> となり(30.10.8/p5)、厳密には path(string_type&& source, format fmt = auto_format) コンストラクタが選択される(30.10.8.4.1/p4)

*3:30.10.8.1/p2: "Except in a root-name, multiple successive directory-separator characters are considered to be the same as one directory-separator character."

定数式を要求するコンセプト

C++2a(C++20) Conceptを利用した「ある式が定数式であること」を要求する制約式の定義。

型パラメータTに対して「T::size()コンパイル時に評価されること」を要求するコンセプトHasConstantSizeの定義例。requires式(requires-expression)中の typename type-name; 構文(type-requirement)は、type-name が有効な型であることを表明する。例示コードのように、クラステンプレート特殊化に対しては完全型が要求されない。*1

// C++2a
template<auto> struct require_constant;

template<class T>
concept HasConstantSize = requires {
  typename require_constant<T::size()>;
};

C++2a標準Rangeライブラリ std::range::split_view 定義で同テクニックを利用している。N4810*2 24.7.8.2より一部引用。

namespace std::ranges {
  template<auto> struct require-constant;  // exposition only

  template<class R>
  concept tiny-range =  // exposition only
    SizedRange<R> &&
    requires { typename require-constant<remove_reference_t<R>::size()>; } &&
    (remove_reference_t<R>::size() <= 1);

  // (snip)
}

関連URL

*1:N4810 7.5.7.2: "A type-requirement that names a class template specialization does not require that type to be complete."

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

書式指定子の入れ子

プログラミング言語Pythonstr.formatf-string*1 において、書式指定子部のみ1段階の入れ子が許容される。下記コードではいずれも文字列 Hello!!!!! が得られる。

msg = 'Hello'
'{:!<10}'.format(msg)
f'{msg:!<10}'

f, a, w = '!', '<', 10
'{:{}{}{}}'.format(msg, f, a, w)
f'{msg:{f}{a}{w}}'

# 各typeフィールドを明示
f'{msg:{f:s}{a:s}{w:d}s}'

Top-level format specifiers may include nested replacement fields. These nested fields may include their own conversion fields and format specifiers, but may not include more deeply-nested replacement fields. The format specifier mini-language is the same as that used by the string .format() method.

Lexical analysis, Formatted string literals

*1:formatted string literal