yohhoyの日記

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

std::coutの実体はどこ?

C++標準ライブラリで定義されるグローバルオブジェクトstd::cout等の宣言と定義についてメモ。

GCC/libstdc++およびClang/libcxxライブラリ実装では、宣言型(std::ostream)と実体定義の型(char[])を意図的に変えている。型の不一致は厳密にはC++仕様違反*1だが、標準ヘッダ<iostream>提供グローバルオブジェクトの生存期間要件を満たすためのHACKと考えられる。たぶん。

C++03 27.3/p2より引用(下線部は強調)。

Mixing operations on corresponding wide- and narrow-character streams follows the same semantics as mixing such operations on FILEs, as specified in Amendment 1 of the ISO C standard. The objects are constructed, and the associations are established at some time prior to or during first time an object of class ios_base::Init is constructed, and in any case before the body of main begins execution.264) The objects are not destroyed during program execution.265)
脚注264) If it is possible for them to do so, implementations are encouraged to initialize the objects earlier than required.
脚注265) Constructors and destructors for static objects can access these objects to read input from stdin or write output to stdout or stderr.

GCC/libstdc++

宣言:https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.1.0/libstdc++-v3/include/std/iostream#L61

extern ostream cout;  /// Linked to standard output

実装:https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.1.0/libstdc++-v3/src/c++98/globals_io.cc#L50-L59

// Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_ostream[sizeof(ostream)]
  __attribute__ ((aligned(__alignof__(ostream))));
fake_ostream cout;

Clang/libcxx

宣言:https://github.com/llvm/llvm-project/blob/llvmorg-12.0.1/libcxx/include/iostream#L53

extern ostream cout;

実装:https://github.com/llvm/llvm-project/blob/llvmorg-12.0.1/libcxx/src/iostream.cpp#L38-L42

alignas(ostream) char cout[sizeof(ostream)];

可読性のため各種マクロは展開済み。

MSVC

宣言:https://github.com/microsoft/STL/blob/d5feb03f320ccaa8e214e3010c40c9c5afd686e2/stl/inc/iostream#L40

extern ostream cout;

実装:https://github.com/microsoft/STL/blob/d5feb03f320ccaa8e214e3010c40c9c5afd686e2/stl/src/cout.cpp#L15-L22

static filebuf fout(__acrt_iob_func(1));
extern ostream cout(&fout);

可読性のため各種マクロは展開済み。__acrt_iob_func関数はCランタイム(CRT)の内部関数であり実装非公開。

関連URL

*1:C++03 3.5/p10: "(snip), the types specified by all declarations referring to a given object or function shall be identical, except that declarations for an array object (snip). A violation of this rule on type identity does not require a diagnostic."

realloc(ptr, 0)は廃止予定

C標準ライブラリrealloc関数に対して、サイズ0を指定すべきでない。

realloc(ptr, 0)によってptrが指すメモリブロックが解放(free(ptr)相当)される保証はない。この動作は ISO C および POSIX それぞれで明言されている。JPCERT MEM04-C サイズ0のメモリ割り当てを行わない も参照のこと。

C標準ライブラリ仕様に文面解釈の幅があったため処理系ごとに動作が異なる状況となっており、次期C2x(C23)標準ライブラリでは「reallocへのサイズ0指定」は廃止予定の機能(obsolescent feature)とされる。これ以外の挙動については現行C17と同じ。

C90/C99/C11仕様

C11 7.22.3.5/p2より引用。C90 7.10.3, C99 7.20.3.4/p2も同一文面。

If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size. Otherwise, if ptr does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to the free or realloc function, the behavior is undefined. If memory for the new object cannot be allocated, the old object is not deallocated and its value is unchanged.

C17仕様

C17 7.22.3.5/p3より引用(下線部は強調)。

If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size. Otherwise, if ptr does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to the free or realloc function, the behavior is undefined. If size is nonzero and memory for the new object is not allocated, the old object is not deallocated. If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated. If the old object is not deallocated, its value shall be unchanged.

C2x仕様

C2x WD(N2596) 7.22.3.5/p3, 7.31.14/p2より引用(下線部は強調)。

If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size. Otherwise, if ptr does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to the free or realloc function, or if the size is zero, the behavior is undefined. If memory for the new object is not allocated, the old object is not deallocated and its value is unchanged.

Invoking realloc with a size argument equal to zero is an obsolescent feature.

POSIX仕様(2017)

IEEE Std 1003.1-2017より一部引用。FUTURE DIRECTIONS(informative)節にてWG14 C2x標準化ステータスへの言及あり。

DESCRIPTION
(snip) If the size of the space requested is zero, the behavior shall be implementation-defined: either a null pointer is returned, or the behavior shall be as if the size were some non-zero value, except that the behavior is undefined if the returned pointer is used to access an object. If the space cannot be allocated, the object shall remain unchanged.
(snip)

FUTURE DIRECTIONS(informative)
This standard defers to the ISO C standard. While that standard currently has language that might permit realloc (p, 0), where p is not a null pointer, to free p while still returning a null pointer, the committee responsible for that standard is considering clarifying the language to explicitly prohibit that alternative.

関連URL

レンジ to コンテナ変換

次期C++2b(C++23)標準ライブラリに向けて、Rangesから各種コンテナ型への直接変換サポートが検討されている。(PDF)P1206R6では下記の機能追加/拡張を提案している。

  • コンテナ型Cへの変換std::ranges::to<C>レンジアダプタ(range adaptor)
  • 標準コンテナへのstd::from_rangeコンストラクタ追加*1
#include <ranges>
#include <vector>

bool is_prime(int n);

// 100未満の素数を列挙するRange
auto rng = std::views::iota(1)
  | std::views::filter(is_prime)
  | std::views::take_while([](int x) { return x < 100; });

// C++20: 一部Rangeではcommon_viewへの変換が必要
auto cv = rng | std::views::common;
std::vector vec( std::begin(cv), std::end(cv) );
// C++2b(P1206R6)
auto rng = /* (同上) */;

// std::ranges::to<C>レンジアダプタ
auto vec = std::ranges::to<std::vector>( rng );  // 関数記法
auto vec = rng | std::ranges::to<std::vector>;   // パイプライン記法
// std::from_rangeコンストラクタ
std::vector vec( std::from_range, rng );

std::ranges::to<C>レンジアダプタでは、あらゆる実現手段を用いてRange→コンテナ変換を試みる。コンパイル時に下記パターンを試行する:

1. レンジからコンテナCを直接構築
2. std::from_rangeタグ付きコンストラクタを用いたコンテナC構築
3. イテレータペア:std::range::begin(r), std::range::end(r)からコンテナC構築
4. 末尾への要素追加:コンテナC構築後にRange要素を順次末尾に追加

  • 変換先コンテナCが容量指定可能ならば、reserveメンバ関数によりメモリ事前確保を試みる。
  • 末尾への要素追加にはpush_backまたはinsertメンバ関数を利用する。

5. 入れ子Range対応:変換先コンテナ要素が子Rangeかつ変換元Range要素も子Rangeならば、各要素(子Range)に対するstd::ranges::to結果からコンテナCを構築

  • 例:std::list<std::list<int>>からstd::vector<std::vector<double>>への一発変換など。

6. いずれの方式も利用不可ならばコンパイルエラー(ill-formed)

関連URL

*1:std::from_range はコンストラクオーバーロード選択(タグディスパッチ)用の定数。

Last Piece of ラムダ式への属性指定

C++20現在の言語仕様では、ラムダ式に対して(普通のプログラマが期待するであろう)属性指定は行えない。C++2b(C++23)に向けた提案(PDF)P2173R0が進行中。*1

下記コードはC++構文規則上は許容されるものの、ラムダ式戻り値に対するnodiscard属性指定ではなく、ラムダ式により生成されるクロージャクラスClosureの関数呼び出し演算子オーバーロード関数型int Closure::operator()() constに対する属性指定と解釈される。

// C++20: ラムダ戻り値にnodiscard属性を指定 ??
auto lm = [] () [[nodiscard]] { return 42; };
// GCC 11.0/-std=c++2a
//  warning: 'nodiscard' attribute can only be applied to functions or to class or enumeration types
// Clang 11.1/-std=c++2a
//  error: 'nodiscard' attribute cannot be applied to types

C++2bにおけるラムダ式戻り値に対する属性指定は、パラメータ宣言部の直前に記述する構文となる予定。C++11~C++20現在の関数型に対する属性指定*2はそのまま維持される。

// C++2b: ラムダ戻り値にnodiscard属性を指定
auto lm = [] [[nodiscard]] () { return 42; };
//           ^^^^^^^^^^^^^

std::integral auto f = /*...*/;
// C++2b: ジェネリックラムダ戻り値にnodiscard属性を指定
auto glm = [f] <typename T> requires std::integral<T>
    [[nodiscard]] (T n) mutable noexcept -> T { return n * f++; };
//  ^^^^^^^^^^^^^

おまけ:GCC 9.3以降では上記C++2b構文が期待通りに解釈される(言語バージョン指定-std=c++NNは不問)。GCC Bugzilla 90333 によれば意図的な仕様拡張か?Clang 13ではC++2b以降の仕様である旨が表示される*3

関連URL

*1:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1018r8.html

*2:もし本当に必要ならば、構文的にはnoexcept指定(exception-specification)の直後、戻り値型後置(trailing-return-type)の直前に属性指定を記述する。

*3:"warning: an attribute specifier sequence in this position is a C++2b extension [-Wc++2b-extensions]"

可変長コンセプト×畳み込み式: The glass is half full or half empty?

C++20コンセプトと論理演算子(&&, ||)による畳み込み式(fold expression)の関係について。本記事の内容はStackOverflowで見つけた質問と回答に基づく。

まとめ:&&||による畳み込み式を用いた制約式(constraint-expression)は機能するものの、コンセプト間の包摂関係(subsumption relation)は期待通りに成り立たない。

#include <concepts>

// コンセプト any_of<T, Us...>
// 型Tが型リストUs..のいずれかと一致する?
template <typename T, typename... Us>
concept any_of = (std::same_as<T, Us> || ...);

template <typename T>
constexpr int f(T) { return 1; }  // #1

template <typename T>
  requires any_of<T, int, double>
constexpr int f(T) { return 2; }  // #2
// または
template <any_of<int, double> T> 
constexpr int f(T) { return 2; }  // #2

static_assert(f(42) == 2);    // OK: #2 int型
static_assert(f(3.14) == 2);  // OK: #2 double型
static_assert(f(0.f) == 1);   // OK: #1 float型
static_assert(f('X') == 1);   // OK: #1 char型

C++20コンセプト仕様では、畳み込み式(std::same_as<T, Us> || ...)それ自体で一つの原始制約(atomic constraint)を構成する。オーバーロード解決のために制約式の包摂関係を求める(≒強弱の判定)正規化(normalization)過程で、std::same_as<T, Us[0]>std::same_as<T, Us[1]>...のようには展開解釈されない。*1

// 畳み込み式による(可変長)コンセプト定義
template <typename T, typename... Us>
concept any_of = (std::same_as<T, Us> || ...);

template <any_of<int, double> T> 
constexpr int f(T) { return 2; }  // #2

template <std::same_as<double> T>
constexpr int f(T) { return 3; }  // #3

static_assert(f(42) == 2);    // OK: #2の制約のみ満たす
static_assert(f(3.14) == 3);  // NG: #2,#3間でオーバロード解決が曖昧

可変長コンセプトをあきらめて制約式(std::same_as<T, U1> || std::same_as<T, U2>)とすれば包摂関係が成立し、自然なオーバーロード選択が行われる。つまり制約any_of2<T, int, double>よりも制約std::same_as<T, double>の方がより強い制約(more constrained)と解釈される。

// 畳み込み式を利用しない(2型版)コンセプト定義
template <typename T, typename U1, typename U2>
concept any_of2 = (std::same_as<T, U1> || std::same_as<T, U2>);

template <any_of2<int, double> T> 
constexpr int f(T) { return 2; }  // #2

template <std::same_as<double> T>
constexpr int f(T) { return 3; }  // #3

static_assert(f(42) == 2);    // OK: #2
static_assert(f(3.14) == 3);  // OK: #3は#2より強く制約される

関連URL

*1:C++20 §13.5.1.1/p1 Note: "(snip) For the purpose of exposition, conjunction is spelled using the symbol ⋀ and disjunction is spelled using the symbol ⋁. (snip)"

requires式中でのコンセプト制約表現には要注意

C++20 requires式(requires-expression) において、コンセプトや条件式を用いた制約(constraints)表現には注意が必要。

下記コードのように式std::signed_integral<decltype(N)>N == 42をrequires式中に単に記載すると単純要件(simple-requirement)となり、式の妥当性のみがチェックされプログラマが意図する制約は行われない。std::signed_integral<decltype(N)>N == 42の評価結果は利用されず、それ自身は常に有効な式となるため。*1

#include <concepts>

// 符号付き整数型の定数42を表すコンセプト(?)
template <auto N>
concept magic_number = requires {
  std::signed_integral<decltype(N)>;  // ★
  N == 42;  // ★
};

static_assert(  magic_number<42> );   // OK
static_assert( !magic_number<42u> );  // NG !?
static_assert( !magic_number<0> );    // NG !?

requiresキーワードを用いて入れ子要件(nested-requirement)として記述とするか、単に制約式の&&(conjunction; 連言/合接)として記述する。両記述はセマンティクス上ほぼ等価だが、後者の方がコンセプトベースのオーバーロード解決における包摂関係(→id:yohhoy:20190903)を自然に表現できる*2。Simpe is the best.

// 符号付き整数型の定数42を表すコンセプト
template <auto N>
concept magic_number = requires {
  requires std::signed_integral<decltype(N)>;
  requires (N == 42);
};
// または
template <auto N>
concept magic_number = std::signed_integral<decltype(N)> && (N == 42);

static_assert(  magic_number<42> );   // OK
static_assert( !magic_number<42u> );  // OK
static_assert( !magic_number<0> );    // OK

関連URL

*1:C++20 13.3/p8: "A concept-id is a simple-template-id where the template-name is a concept-name. A concept-id is a prvalue of type bool, and does not name a template specialization. A concept-id evaluates to true if the concept's normalized constraint-expression is satisfied by the specified template arguments and false otherwise."

*2:https://wandbox.org/permlink/xhGW29ycmxLWJIR7

HRESULT型からのエラーメッセージ取得

WindowsOS環境のHRESULT型エラーコードからエラーメッセージ文字列へのお手軽変換。Microsoft Visual C++(MSVC)限定。

#include <windows.h>
#include <system_error>

std::string get_message(HRESULT hr)
{
  return std::system_category().message(hr);
}

// GetLastError()戻り値などDWORD型 Windows Error Code の場合は、
// HRESULT_FROM_WIN32マクロによりHRESULT型へと事前変換する。

上記コードではWindows OSのロケールに応じた文字列(日本語OSなら日本語のメッセージ)が取得される。英語メッセージで固定したい場合などは、FormatMessage関数を自前で呼び出す必要がある。

おまけ:FormatMessage関数のdwLanguageId引数=0で実装されているため*1SetThreadUILanguage関数でスレッドのLANGIDを事前に変更しておく案もある。シングルスレッドプログラムならこれでもいいか...

関連URL

*1:"If you pass in zero, FormatMessage looks for a message for LANGIDs in the following order: 1.Language neutral / 2.Thread LANGID, based on the thread's locale value / 3.(snip)"