yohhoyの日記

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

std::search_nアルゴリズムとゼロ長サブシーケンス一致

N要素からなるサブシーケンス検索を行うC++標準アルゴリズムstd::search_nでは、“0個の任意要素からなるサブシーケンス” は常に先頭位置にマッチする。

#include <algorithm>

int arr[5] = {0, 10, 10, 20, 30};

// 2個の要素10からなるサブシーケンス
auto itr1 = std::search_n(arr, arr + 5, 2, 10);
assert(itr1 == arr + 1);  // index=1

// 2個の要素20からなるサブシーケンス
auto itr2 = std::search_n(arr, arr + 5, 2, 20);
assert(itr2 == arr + 5);  // NOT FOUND

// 0個の要素42からなるサブシーケンス
auto itr3 = std::search_n(arr, arr + 5, 0, 42);
assert(itr3 == arr + 0);  // index=0

C++03 25.1.9/p4-6より引用。

template<class ForwardIterator, class Size, class T>
  ForwardIterator
    search_n(ForwardIterator first, ForwardIterator last, Size count,
           const T& value);

template<class ForwardIterator, class Size, class T,
         class BinaryPredicate>
  ForwardIterator
    search_n(ForwardIterator first, ForwardIterator last, Size count,
          const T& value, BinaryPredicate pred);

4 Requires: Type T is EqualityComparable (20.1.1), type Size is convertible to integral type (4.7, 12.3).
5 Effects: Finds a subsequence of equal values in a sequence.
6 Returns: The first iterator i in the range [first, last - count) such that for any non-negative integer n less than count the following corresponding conditions hold: *(i + n) == value, pred(*(i + n),value) != false. Returns last if no such iterator is found.

関連URL

requires制約とテンプレート特殊化の関係

C++20コンセプトで導入されたrequires節と、テンプレート特殊化の組み合わせには注意が必要。

例えば制約付きプライマリテンプレート(#1)に対してdouble型で明示的特殊化(#2)しようとしても、下記記述コードでは#2定義でill-formedになる。これは#1のrequires節によりT=doubleのときプライマリテンプレート不在とみなされるため。

#include <concepts>

// #1 プライマリテンプレート(?)
template<typename T>
  requires (!std::floating_point<T>)
int f(T) { return 0; }

// #2 double型による特殊化
template<>
int f(double) { return 1; }  // NG

assert( f(42) == 0 );
assert( f(3.14) == 1 );

テンプレートの明示的特殊化ではなく通常関数によるオーバーロードを行うか、プライマリテンプレートの関連制約(requires節)を削除すれば期待通り動作する。

// #1 制約付きテンプレート
template<typename T>
  requires (!std::floating_point<T>)
int f(T) { return 0; }

// #2 double型でオーバーロード
int f(double) { return 1; }  // OK
// #1 プライマリテンプレート
template<typename T>
int f(T) { return 0; }

// #2 double型による特殊化
template<>
int f(double) { return 1; }  // OK

C++20 13.9.3/p12より引用。

[Note: An explicit specialization of a constrained template is required to satisfy that template's associated constraints (13.5.2). The satisfaction of constraints is determined when forming the template name of an explicit specialization in which all template arguments are specified (13.3), or, for explicit specializations of function templates, during template argument deduction (13.10.2.6) when one or more trailing template arguments are left unspecified. -- end note]

関連URL

電子書籍"Pro TBB: C++ Parallel Programming with Threading Building Blocks"

Intel oneTBB(oneAPI Threading Building Blocks)によるC++並列プログラミングの電子書籍。PDF形式(754頁)がCC-NC-ND 4.0ライセンスで公開されている。

コンセプトのパラメータ置換失敗はハードエラーではない

C++2a(C++20)コンセプトにおける原始制約(atomic constraint)では、パラメータ置換(parameter mapping)の失敗はハードエラー(ill-formed)を引き起こさず、その制約式を満たさない(not satisfied)と解釈される。C++17現在はstd::void_tstd::conjunctionを駆使した難解なテンプレートメタプログラミング技法が要求されるが、C++2aでは単純な制約式記述によるコンセプト定義により代替される。はず。\\\\٩( 'ω' )و ////

ある型TT::value_type = int型を定義するか確認するメタ関数/コンセプトIntContainerの実装例。

// C++17
#include <type_traits>

// 補助メタ関数 HasValueType
template <class, class = std::void_t<>>
struct HasValueType
  : std::false_type {};
template <class T>
struct HasValueType<T, std::void_t<typename T::value_type>>
  : std::true_type {};

// 補助メタ関数 IntValueType
template <typename T>
struct IntValueType
  : std::is_same<typename T::value_type, int> {};

template <typename T>
using IntContainer
  = std::conjunction<HasValueType<T>, IntValueType<T>>;
// std::conjunctionメタ関数を用いてメタ関数HasValueTypeが真のときのみ
// メタ関数IntValueTypeをインスタンス化する必要がある(&&ではダメ)

static_assert( IntContainer<std::list<int>>::value);
static_assert(!IntContainer<std::list<char>>::value);
static_assert(!IntContainer<int>::value);
// C++2a
#include <concepts>

template <typename T>
concept IntContainer = std::same_as<typename T::value_type, int>;
// T::value_typeがint以外 or 無効な場合は制約式がnot satisfiedとなる

static_assert( IntContainer<std::list<int>>);
static_assert(!IntContainer<std::list<char>>);
static_assert(!IntContainer<int>);

ノート:制約式IntContainer<int>の正規形はstd::same_as<typename T::value_type, int>(T=int)よりstd::is_same_v<T↦typename int::value_type, U↦int>std::is_same_v<T↦int, U↦typename int::value_type>となり*1typename int::value_typeは無効な型のため原始制約はいずれも満たされない(not satisfied)。たぶん\( 'ω' )/

C++2a DIS(N4681) 13.5.1.2/p1, p3, 13.5.3/p1-2より一部引用(下線部は強調)。

1 An atomic constraint is formed from an expression E and a mapping from the template parameters that appear within E to template arguments that are formed via substitution during constraint normalization in the declaration of a constrained entity (and, therefore, can involve the unsubstituted template parameters of the constrained entity), called the parameter mapping (13.5.2). (snip)

3 To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied. Otherwise, the lvalue-to-rvalue conversion (7.3.1) is performed if necessary, and E shall be a constant expression of type bool. The constraint is satisfied if and only if evaluation of E results in true. If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required. (snip)

1 The normal form of an expression E is a constraint (13.5.1) that is defined as follows:

  • (snip)
  • The normal form of a concept-id C<A1, A2, ..., An> is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required. [Example:
template<typename T> concept A = T::value || true;
template<typename U> concept B = A<U*>;
template<typename V> concept C = B<V&>;

Normalization of B's constraint-expression is valid and results in T::value (with the mapping T ↦ U*) ∨ true (with an empty mapping), despite the expression T::value being ill-formed for a pointer type T. Normalization of C's constraint-expression results in the program being ill-formed, because it would form the invalid type V&* in the parameter mapping. --end example]

  • The normal form of any other expression E is the atomic constraint whose expression is E and whose parameter mapping is the identity mapping.

2 The process of obtaining the normal form of a constraint-expression is called normalization. [Note: Normalization of constraint-expressions is performed when determining the associated constraints (13.5.1) of a declaration and when evaluating the value of an id-expression that names a concept specialization (7.5.4). --end note]

関連URL

*1:ここではパラメータマッピングを ↦ 記号で表記する。記号 ∧ は制約式の conjunction を表す(13.5.1.1)。

コンセプトと短絡評価

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