yohhoyの日記

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

コンセプトと短絡評価

C++2a(C++20)コンセプトの制約式(constraint-expression)では、論理演算&&, ||は短絡評価される。*1

C++17現在のテンプレートメタプログラミングではstd::conjunction, std::disjunctionメタ関数を必要とするが、C++2aコンセプト導入により自然な制約記述が可能となる。

T型が「std::atomic<T>が常にロックフリー(lock free)か否か」を判定するメタ関数は、std::conjunctionメタ関数を利用して下記の通りに記述できる。

// C++17
#include <atomic>
#include <type_traits>

template <typename T>
struct is_lock_free_impl
  : std::bool_constant<std::atomic<T>::is_always_lock_free> { };

template <typename T>
using is_lock_free
  = std::conjunction<std::is_trivially_copyable<T>, is_lock_free_impl<T>>;

メタ関数is_lock_freeの定義で&&演算子を使った場合、左辺std::is_trivially_copyable<T>::valueの真偽値に関わらず右辺is_lock_free_impl<T>::valueインスタンス化が行われる。

conjunction/disjunctionと短絡インスタンス化
// C++2a
#include <atomic>
#include <type_traits>

// コンセプトis_lock_free
template <typename T>
concept is_lock_free
  = std::is_trivially_copyable_v<T> && std::atomic<T>::is_always_lock_free;

// (std::atomic<int>型がロックフリーに振る舞う処理系を仮定)
static_assert( is_lock_free<int>, "int is lock-free" );  // OK

// 非Trivially Copyableなクラス型X
struct X { ~X() {} };
static_assert( !is_lock_free<X>, "X is not lock-free" );  // OK

C++2a DIS(N4681) 13.5.1.1/p1-3より引用(下線部は強調)。

1 There are two binary logical operations on constraints: conjunction and disjunction. [Note: These logical operations have no corresponding C++ syntax. For the purpose of exposition, conjunction is spelled using the symbol ∧ and disjunction is spelled using the symbol ∨. The operands of these operations are called the left and right operands. In the constraint AB, A is the left operand, and B is the right operand. --end note]
2 A conjunction is a constraint taking two operands. To determine if a conjunction is satisfied, the satisfaction of the first operand is checked. If that is not satisfied, the conjunction is not satisfied. Otherwise, the conjunction is satisfied if and only if the second operand is satisfied.
3 A disjunction is a constraint taking two operands. To determine if a disjunction is satisfied, the satisfaction of the first operand is checked. If that is satisfied, the disjunction is satisfied. Otherwise, the disjunction is satisfied if and only if the second operand is satisfied.

関連URL

*1:C++コンセプト制約式を用いたオーバーロード解決においては、演算子 &&, ||, ! がブール代数的な論理積論理和/論理否定から期待されるセマンティクスとは多少異なる動作をする。詳細は C++コンセプトとド・モルガンの法則 を参照のこと。

関数/ラムダ式への値束縛

プログラミング言語Pythonにおいて、関数やラムダ式にローカル変数の「値」を束縛する方法。

下記コードでは “引数x定義時の値nで冪乗” する関数を個別生成するつもりが、実際には “引数x変数nの値で冪乗” する同一の関数が生成される。forループ終了後の変数nは値3となっており、プログラマの期待に反する実行結果となる。

# 関数
ops = []
for n in range(4):
    def fn(x):
        return x ** n
    ops.append(fn)
print([f(2) for f in ops])  # [8, 8, 8, 8]

# ラムダ式
ops = []
for n in range(4):
    ops.append(lambda x: x ** n)
print([f(2) for f in ops])  # [8, 8, 8, 8]

関数やラムダ式のデフォルト引数値は定義時に評価される*1ことを利用し、次のようにローカル変数の値を束縛できる。

# 関数
ops = []
for n in range(4):
    def fn(x, n=n):
        return x ** n
    ops.append(fn)
print([f(2) for f in ops])  # [1, 2, 4, 8]

# ラムダ式
ops = []
for n in range(4):
    ops.append(lambda x, n=n: x ** n)
print([f(2) for f in ops])  # [1, 2, 4, 8]

他の実装手段として、関数/ラムダ式入れ子にし外側だけ評価する方法や、 functools.partial 関数を利用する方法がある。

# 関数の入れ子
ops = []
for n in range(4):
    def fn(n):
        def fn0(x):
            return x ** n
        return fn0
    ops.append(fn(n))
print([f(2) for f in ops])  # [1, 2, 4, 8]

# ラムダ式の入れ子と即時評価
ops = []
for n in range(4):
    fn = (lambda n: lambda x: x ** n)(n)
    ops.append(fn)
print([f(2) for f in ops])  # [1, 2, 4, 8]
from functools import partial

# 関数+partial
ops = []
for n in range(4):
    def fn(x, n):
        return x ** n
    ops.append(partial(fn, n=n))
print([f(2) for f in ops])  # [1, 2, 4, 8]

# ラムダ式+partial
ops = []
for n in range(4):
    fn = lambda x, n: x ** n
    ops.append(partial(fn, n=n))
print([f(2) for f in ops])  # [1, 2, 4, 8]

関連URL

std::functionのムーブ操作はムーブするとは言っていない

C++標準ライブラリstd::functionのムーブ操作(ムーブコンストラクタ/ムーブ代入演算子)は保持する呼出し可能なオブジェクト(callable object)を必ずしもムーブせず、条件によってはコピーが行われる可能性がある。( ゚Д゚)ハァ?

C++標準ライブラリ仕様ではstd::functionムーブ操作の結果として、下記の2種類の選択肢を与える:

  • 呼出し可能オブジェクトの所有権がムーブ先に移動し、ムーブ元のstd::functionオブジェクトは呼出し不可能となる。
  • 呼出し可能オブジェクトはコピーされ、ムーブ先/ムーブ元いずれのstd::functionオブジェクトも呼出し可能となる。

下記コードではラムダ式によるクロージャオブジェクトのコピー/ムーブ判別に、スマートポインタstd::shared_ptrの参照カウント(use_count())を利用している。*1

// 動作追跡用スマートポインタ(参照カウントにのみ着目)
auto sp = std::make_shared<int>(42);

// fはスマートポインタを保持するクロージャオブジェクトを保持する
std::function<long()> f = [sp=std::move(sp)]{ return sp.use_count(); };
std::cout << f() << " ";  // 1を出力

// fからgへのムーブ操作
std::function<long()> g = std::move(f);
// クロージャオブジェクトがムーブされた場合は参照カウント=1に
// クロージャオブジェクトがコピーされた場合は参照カウント=2となる
std::cout << g() << " ";  // ★1 or 2を出力

// ★ムーブ後のfを呼び出す
try {
  std::cout << f() << " ";  // コピーされた場合は2を出力
  // 注: ソースコード上は"ムーブ後"のためこの動作を期待すべきではない
} catch (const std::bad_function_call&) {
  // ムーブ操作によりfが無効となりbad_function_call例外が送出される
  std::cout << ". ";
}

// ムーブ後のfを明示的にクリアする
f = nullptr;
std::cout << g();  // 1を出力

GCC 10.1による実行結果:

1 1 . 1

Clang 10.0による実行結果:

1 2 2 1

C++標準規格ではサイズが小さい呼出し可能オブジェクトを格納する際は動的メモリ確保を避けるよう推奨(encourage)しており、標準ライブラリ実装では Small Buffer Optimization(SBO) 技法(→id:yohhoy:20120428)が用いられる。GCCとClangではSBO利用の閾値が異なっており、前述の実行結果になったと考えられる。

auto sp = std::make_shared<int>(42);
struct { char x[1000]; } large;

// データメンバにlargeを含む大きなサイズのクロージャオブジェクトを保持するため
// SBOは無効となり動的確保メモリ上にオブジェクト構築されると期待できる
std::function<long()> f = [sp=std::move(sp), large] {
    (void)large;  // (largeを使った処理)
    return sp.use_count();
  };

// fからgへのムーブ操作
std::function<long()> g = std::move(f);
std::cout << g();  // 1を出力(と期待できる)

std::functionクラステンプレートはムーブ元オブジェクトが “有効だが未規定な状態(valid but unspecified state)”(→id:yohhoy:20120616) としか要求せず、また演算子オーバーロードoperator()仕様をふまえるとムーブコンストラクタ/代入演算子がコピー/ムーブ処理のいずれを行っても標準準拠といえる。もうちょっと何とかならんかったのか。*2

std::moveの代わりに [C++]std::exchangeによるmoveしてリセットするイディオムの御紹介 - 地面を見下ろす少年の足蹴にされる私 で紹介しているイディオムを用いると、C++プログラマの意図通りの動作を保証できる。

// fからgへの確実なムーブ処理
std::function<long()> g = std::exchange(f, {});

C++17 20.3.25, 23.14.13.2.1/p5-6, p16, p18-19より引用(下線部は強調)。

valid but unspecified state
a value of an object that is not specified except that the object's invariants are met and operations on the object behave as specified for its type
[Example: If an object x of type std::vector<int> is in a valid but unspecified state, x.empty() can be called unconditionally, and x.front() can be called only if x.empty() returns false. -- end example]

function(function&& f);
5 Postconditions: If !f, *this has no target; otherwise, the target of *this is equivalent to the target of f before the construction, and f is in a valid state with an unspecified value.
6 Throws: shall not throw exceptions if f's target is a specialization of reference_wrapper or a function pointer. Otherwise, may throw bad_alloc or any exception thrown by the copy or move constructor of the stored callable object. [Note: Implementations are encouraged to avoid the use of dynamically allocated memory for small callable objects, for example, where f's target is an object holding only a pointer or reference to an object and a member function pointer. -- end note]

function& operator=(function&& f);
16 Effects: Replaces the target of *this with the target of f.

function& operator=(nullptr_t) noexcept;
18 Effects: If *this != nullptr, destroys the target of this.
19 Postconditions: !(*this).

関連URL

*1:ラムダ式でキャプチャした変数は、クロージャ型の非staticデータメンバとして保持される(8.1.5.2/p6, p15)。クロージャ型はdefaultedなコピー/ムーブコンストラクタを生成する(8.1.5.1/p11)。スマートポインタ std::shared_ptr では、ムーブコンストラクタで所有権を移動すると保証している(23.11.2.2.1/p23)。

*2:ソースコードが表現するセマンティクスは “ムーブ後のオブジェクト” となるため、プログラマはコピー処理が行われることを期待すべきでない。というか、コピーが必要ならソースコードにもそう書くに決まっと(#゚Д゚)ルァ!!

std::functionのテンプレート推論ガイド

C++17標準ライブラリで導入されたstd::functionクラステンプレートの推論ガイド(deduction guide)についてメモ。

#include <functional>

// 関数ポインタから推論
int f(int n) { return n; }
std::function f1 = f;  // OK: function<int(int)>

// ラムダ式から推論
std::function f2 = [](int n){ return n; };  // OK: function<int(int)>

// ファンクタから推論
struct Functor {
  int m_;
  int operator()(int n) const { return n * m_; }
};
std::function f3 = Functor{42};  // OK: function<int(int)>

// データメンバ/メンバ関数から推論?
struct S {
  int md = 1;
  int mf(int n) { return md += n; }
};
std::function f4 = &S::md;  // NG: ill-formed
std::function f5 = &S::mf;  // NG: ill-formed

// OK: テンプレート型引数を明示指定
std::function<int&(S&)>     f4r = &S::md;
std::function<int(S&, int)> f5r = &S::mf;
std::function<int&(S*)>     f4p = &S::md;
std::function<int(S*, int)> f5p = &S::mf;

S s;
assert(f4r( s) == 1 && f5r( s, 1) == 2);
assert(f4p(&s) == 2 && f5p(&s, 1) == 3);

C++17 23.14.13.2/p4, 23.14.13.2.1/p12-13より引用。

template<class R, class... ArgTypes>
  function(R(*)(ArgTypes...)) -> function<R(ArgTypes...)>;

template<class F> function(F) -> function<see below>;

4 [Note: The types deduced by the deduction guides for function may change in future versions of this International Standard. -- end note]

template<class F> function(F) -> function<see below>;
12 Remarks: This deduction guide participates in overload resolution only if &F::operator() is well-formed when treated as an unevaluated operand. In that case, if decltype(&F::operator()) is of the form R(G::*)(A...) cv &opt noexceptopt for a class type G, then the deduced type is function<R(A...)>.
13 [Example:

void f() {
  int i{5};
  function g = [&](double) { return i; };  // deduces function<int(double)>
}

-- end example]

データメンバ(メンバ変数)/メンバ関数へのポインタからのクラステンプレート型推論は、第1引数のオブジェクト型をポインタ/参照のいずれにするか未決であり、C++17時点では推論ガイド追加を先送りとしている。提案文書 P0433R2 より一部引用。

We suggest allowing function to deduce its template argument when initialized by a function. While it is tempting to add deduction guides for member pointers, we no longer do so as there is a question about whether the first objects should be a pointer or a reference. We are interested in committee feedback. Note that we can always add this post-c++17. Add the following deduction guide at the end of the class definition for function in §20.14.12.2 [func.wrap.func]

(snip)

Note: There are some arbitrary choices in the deductions for member pointers. We would be interested to see if the comittee agrees with having deduction guides for member pointers in addition to function pointers, and if so, whether these are the right choices.

関連URL

std::views::splitで文字列分割

C++2a(C++20) Rangesライブラリstd::views::splitを利用した文字列分割処理。*1
2021-06-24追記:次期C++2b(C++23)標準ライブラリではよりシンプルな実装が可能となる。

split2str関数は「char範囲を区切り文字delimで分割しstd::stringのViewへ変換するレンジアダプタ(range adaptor)」を返す。

// C++2a(C++20)
#include <iomanip>
#include <iostream>
#include <string>
#include <string_view>
#include <ranges>

auto split2str(char delim) {
  return std::views::split(std::views::single(delim))
       | std::views::transform([](auto v) {
           auto cv = v | std::views::common;
           return std::string{cv.begin(), cv.end()};
         });
}

int main() {
  std::string_view s = "apple,banana,cinnamon";
  for (const auto& word : s | split2str(',')) {
    std::cout << std::quoted(word) << "\n";
  }
}
// "apple"
// "banana"
// "cinnamon"

ノート:std::views::splitは区切り要素(char)ではなく “区切りパターン(range)” を引数にとるため、std::views::single(delim)のように単一要素レンジを明示する必要がある雰囲気。splitに左辺値delimを指定すると意図通りに動作せず、キャストによりchar型右辺値を与えるとなぜか動く。C++2a Rangesライブラリは処理系任せの部分が大きく、トラブル時の振る舞いを説明しづらい。単に理解不足かorz

  • 2021-06-23追記:GCC HEAD 12.0.0 20210621(experimental)で再確認したところ、単にstd::views::split(delim) | ...と記述しても期待通りに動作した。

C++2a DIS(N4861) 24.7/p1, 24.7.1/p1より引用。

This subclause defines range adaptors, which are utilities that transform a range into a view with custom behaviors. These adaptors can be chained to create pipelines of range transformations that evaluate lazily as the resulting view is iterated.

A range adaptor closure object is a unary function object that accepts a viewable_range argument and returns a view. For a range adaptor closure object C and an expression R such that decltype((R)) models viewable_range, the following expressions are equivalent and yield a view:

C(R)
R | C

Given an additional range adaptor closure object D, the expression C | D is well-formed and produces another range adaptor closure object such that the following two expressions are equivalent:

R | C | D
R | (C | D)

関連URL

*1:厳密には std::ranges::views::split として宣言される。標準ヘッダ<ranges>にてstd名前空間以下で namespace views = ranges::views; と名前空間エイリアスが定義されている。

プライマリ変数テンプレート無効化の実装例

C++2a(C++20)標準ライブラリ<numbers>では、変数テンプレート(variable template)*1により浮動小数点型(float, double, long double)にあわせた数学定数を提供する。一方、浮動小数点型以外によるプライマリテンプレート利用はill-formedとなることが要請されている。

#include <numbers>

// float型精度の円周率π
float pi_f = std::numbers::pi_v<float>;  // OK
// int型の円周率π?
int pi_i = std::numbers::pi_v<int>;  // NG: ill-formed

N4861 (C++2a DIS) 26.9.2/p3より引用。

A program that instantiates a primary template of a mathematical constant variable template is ill-formed.

GCC/libstdc++

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/numbers

template<typename _Tp>
  using _Enable_if_floating = enable_if_t<is_floating_point_v<_Tp>, _Tp>;

template<typename _Tp>
  inline constexpr _Tp pi_v
    = _Enable_if_floating<_Tp>(3.14159...);

Clang/libcxx

https://github.com/llvm/llvm-project/blob/master/libcxx/include/numbers

template <class T>
inline constexpr bool __false = false;

template <class T>
struct __illformed
{
  static_assert(__false<T>, "...");
};

template <class T> inline constexpr T pi_v =  __illformed<T>{};

template <class T>
concept __floating_point = std::is_floating_point_v<T>;

template <__floating_point T> inline constexpr T pi_v<T> = 3.14159...;

MSVC

https://github.com/microsoft/STL/blob/master/stl/inc/numbers

// stl/inc/xstddef
template <class>
inline constexpr bool _Always_false = false;

template <class _Ty>
struct _Invalid {
  static_assert(_Always_false<_Ty>, "...");
};

template <class _Ty>
inline constexpr _Ty pi_v = _Invalid<_Ty>{};

template <floating_point _Floating>
inline constexpr _Floating pi_v<_Floating> = static_cast<_Floating>(3.14159...);

関連URL

*1:変数テンプレートは C++14 で追加された機能。

C互換ヘッダとstd名前空間

C++標準ライブラリに含まれるC標準ライブラリヘッダ*1と、std名前空間との関係についてメモ。

C++11以降では、下記の振る舞いが保証される:

  • ヘッダcxxx: 名前空間stdにCライブラリの識別子が宣言される。グローバル名前空間にも宣言されるかもしれない。
  • ヘッダxxx.h: グローバル名前空間にCライブラリの識別子が宣言される。名前空間stdにも宣言されるかもしれない。

C++17

C++17 D.5/p3-4より引用(下線部は強調)。29.9.5=Mathematical special functions*2, 21.2.5=byte type operations*3

3 Every other C header, each of which has a name of the form name.h, behaves as if each name placed in the standard library namespace by the corresponding cname header is placed within the global namespace scope, except for the functions described in 29.9.5, the declaration of std::byte (21.2.1), and the functions and function templates described in 21.2.5. It is unspecified whether these names are first declared or defined within namespace scope (6.3.6) of the namespace std and are then injected into the global namespace scope by explicit using-declarations (10.3.3).
4 [Example: The header <cstdlib> assuredly provides its declarations and definitions within the namespace std. It may also provide these names within the global namespace. The header <stdlib.h> assuredly provides the same declarations and definitions within the global namespace, much as in the C Standard. It may also provide these names within the namespace std. -- end example]

C++11/14

C++11 D.5/p2-3より引用(下線部は強調)。C++14でも同様。

2 Every C header, each of which has a name of the form name.h, behaves as if each name placed in the standard library namespace by the corresponding cname header is placed within the global namespace scope. It is unspecified whether these names are first declared or defined within namespace scope (3.3.6) of the namespace std and are then injected into the global namespace scope by explicit using-declarations (7.3.3).
3 [Example: The header <cstdlib> assuredly provides its declarations and definitions within the namespace std. It may also provide these names within the global namespace. The header <stdlib.h> assuredly provides the same declarations and definitions within the global namespace, much as in the C Standard. It may also provide these names within the namespace std. -- end example]

C++03

ヘッダcxxxでは名前空間std配下に宣言し、ヘッダxxx.hでは “名前空間std配下に宣言してからグローバル名前空間へ取り込む(using-declaration)” と強く規定されていた。C++03 D.5/p2-3より引用。

2 Every C header, each of which has a name of the form name.h, behaves as if each name placed in the Standard library namespace by the corresponding cname header is also placed within the namespace scope of the namespace std and is followed by an explicit using-declaration (7.3.3).
3 [Example: The header <cstdlib> provides its declarations and definitions within the namespace std. The header <stdlib.h> makes these available also in the global namespace, much as in the C Standard. -- end example]

関連URL

*1:ヘッダ <cstdlib> とヘッダ <stdlib.h> など

*2:assoc_laguerre, assoc_legendre, beta, comp_ellint_1, comp_ellint_2, comp_ellint_3, cyl_bessel_i, cyl_bessel_j, cyl_bessel_k, cyl_neumann, ellint_1, ellint_2, ellint_3, expint, hermite, laguerre, legendre, riemann_zeta, sph_bessel, sph_legendre, sph_neumann

*3:std::byte 型に対する演算子オーバーロード、to_integer<IntType>関数テンプレート