yohhoyの日記

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

MC Hammer in C++ Standard

プログラミング言語C++標準規格のサンプルコードでビートを刻むMCハマー
"U Can't Touch This" ( 'ω' و(و♪ ƪ( 'ω' ƪ )♪

template<ranges::constant_range R>
void cant_touch_this(R&&);

vector<char> hammer = {'m', 'c'};
span<char> beat = hammer;
cant_touch_this(views::as_const(beat));
  // will not modify the elements of hammer
https://github.com/cplusplus/draft/commit/32535186bc66b3485194b41d5b2107e15c6bd34a

std::ranges::constant_rangeコンセプトやstd::views::as_constレンジアダプタはC++2b(C++23)標準ライブラリへの追加予定機能。

関連URL

std::shared_ptr型と->*演算子とstd::invoke関数

C++標準ライブラリのスマートポインタ型std::shared_ptr<T>では->*演算子オーバーロードを提供しない(注:->はあるよ)。

#include <memory>

struct X { int mf(); };
// メンバ関数ポインタ
int (X::*pmf)() = &X::mf;

// (通常)ポインタ型
X* p = new X;
p->mf();      // OK
(p->*pmf)();  // OK

// std::shared_ptr型
std::shared_ptr<X> sp{ p };
sp->mf();      // OK
(sp->*pmf)();  // NG!
(sp.get()->*pmf)();  // OK: get()でポインタを取出す
(&*sp->*pmf)();      // OK: 暗号じみた記法

C++17で追加されたstd::invoke関数を利用すると、メンバ関数ポインタ呼び出しであっても通常ポインタとスマートポインタ型を統一的に扱える。

#include <functional>

std::invoke(pmf, p);   // OK: X*
std::invoke(pmf, sp);  // OK: std::shared_ptr<X>

// 代替(C++03/11/14)
(*s.*pmf)();   // OK
(*sp.*pmf)();  // OK

マイナー機能のため有用性は低いが、技術的には->*演算子オーバーロードを提供可能。C++14機能までを使った実装例(エッセンスのみ):

// スマートポインタ型
template <typename T>
struct SP {
  T* p_ = nullptr;
  SP(T* p): p_(p) {}

  // ->演算子オーバーロード
  T* operator->() { return p_; }

  // ->*演算子オーバーロード
  template<typename R, typename... Ts>
  auto operator->*(R (T::*pmf)(Ts...)) {
    return [p=p_, pmf](Ts&&... args) -> R {
      return (p->*pmf)(std::forward<Ts>(args)...);
    };
  }
};

// スマートポインタ型
SP<X> sp{ p };
sp->mf();      // OK
(sp->*pmf)();  // OK

関連URL

std::generator<T/T&&/T&/const T&>

次期C++2b(C++23)標準ライブラリのコルーチンサポート型std::generator<Ref>(→id:yohhoy:20220801)の第1テンプレートパラメータRefと、オブジェクトのコピー/ムーブの関係性についてメモ。

まとめ:

  • 利用側にconst参照を提供:const-lvalue参照const T&を指定。
  • 利用側に可変参照を提供:lvalue参照T&を指定。
  • ムーブのみ(Move-only)型:値型Tまたはrvalue参照型T&&を指定。
    • 実動作は両者で同一。“ムーブ” 動作を示唆するT&&の方が好ましい?
  • プリミティブ型など*1:値型Tを指定。
  • 利用側の範囲for:auto&&(→id:yohhoy:20120609)またはconst auto&を推奨。

テンプレートパラメータRefに値型Tまたはその参照型を指定した場合*2、各箇所において発生するコピー/ムーブは下表のとおり:

Ref yield-v yield-cv yield-rv consume-v
T 1 copy 1 copy 0 1 move
T&& 1 copy 1 copy 0 1 move
T& 1 copy 1 copy (ill-formed) 1 copy
const T& 0 0 0 1 copy

凡例:0=コピー/ムーブなし、(ill-formed)=コンパイルエラー

// C++2b
#include <generator>

// ユーザ定義コルーチン
std::generator<??> gen()
{
  T v;
  co_yield v;   // yield-v
  const T cv;
  co_yield cv;  // yield-cv

  // yield-rv
  co_yield T{};
  co_yield std::move(v);
}

// 利用側コード
void consumer()
{
  // consume-v
  for (auto v: gen()) { ... }

  // 下記2パターンは常にコピー/ムーブ発生しない
  for (const auto& cr: gen()) { ... }
  for (auto&& ur: gen()) { ... }
}

ノート:コルーチン内でco_yield std::move(v);を記述しても、即座にはムーブ処理が行われないことに留意。Ref=TT&&であれば利用側コードでムーブ処理が行われる。

関連URL

*1:コピー処理コストの安い型:std::string_view や std::span<T> 型も含まれる。

*2:C++ムーブセマンティクスの文脈では const T&& 型には使い道がない。

std::generator<R>

次期C++2b(C++23)標準ライブラリに追加されるstd::generator<R>クラステンプレートについてメモ。

提案文書P2502R2よりコード引用:*1

// C++2b
#include <functional>
#include <generator>
#include <range>
#include <utility>

std::generator<int> fib() {
  auto a = 0, b = 1;
  while (true) {
    co_yield std::exchange(a, std::exchange(b, a + b));
  }
}

int answer_to_the_universe() {
  auto rng = fib() | std::views::drop(6) | std::views::take(3);
  return std::ranges::fold_left(std::move(rng), 0, std::plus{});
}

コルーチンfibから生成されるフィボナッチ数列 0, 1, 1, 2, 3, 5, 8, 13, 21... のうち、先頭6要素を破棄(drop)した後に3要素のみ取出す(take)遅延評価レンジrngを作成する(この時点ではコルーチン本体処理は開始されない)。rngに対して初期値 0 と算術加算(plus)による畳み込み(fold)操作を行い、結果値 42 を得る。

まとめ:

  • ユーザ定義コルーチンの記述で利用するR型の値を生成するジェネレータ(generator)型。Rangesライブラリとの高い親和性をもつ。
    • co_yield valによる単一値の生成。
    • co_yield elements_of(range)による複数値の逐次生成。
    • コルーチン本体処理は遅延評価される。*2
  • 新しい標準ヘッダ<generator>*3
    • クラステンプレートstd::generator<R, V, Allocator>V, Allocatorは省略可)
    • generatorviewコンセプトとinput_rangeコンセプトのモデル。範囲for文や各種レンジアダプタ(range adaptor)と組み合わせて利用可能。
  • 標準ヘッダ<ranges>の拡張
    • std::ranges::elements_of<R, Allocator>クラステンプレート(Rrangeコンセプトのモデル、Allocatorは省略可)
    • elements_ofはco_yield式に対するタグ型として機能する。
  • ジェネレータ利用側イテレータの提供型
  • generator<R>型(V省略時)
    • 大半のユースケースはテンプレートパラメータR型のみ指定でOK。
    • Reference型=R&&*5Value型=remove_cvref_t<R>
    • 例: generator<int>, generator<string>
  • generator<R, V>
    • 参照プロキシ型の利用や、右辺値を生成する特殊なユースケース向け。
    • Reference型=RValue型=V
    • 例: generator<string_view, string>
  • co_yield val
    • co_yield式の評価結果はvoid
    • co_yield式に指定する右辺値valはコピーされない(R=値型Tのシンプルなケースを想定)。
    • 内部的には指定値へのアドレス情報のみを保持しており(value_ = std::addressof(val))、ジェネレータ利用側イテレータで実体(*value_)をReference型へとキャストする。
    • co_yield式に指定する左辺値valはAwaitableオブジェクト内部にコピー格納され、そのアドレス情報が保持される。
    • 2022-08-10追記:テンプレートパラメータRへ指定する型とコピー/ムーブの関係性は id:yohhoy:20220810 を参照。
  • co_yield elements_of(range)
    • レンジrangeの全要素eに対して、同コルーチンから逐次的に生成(co_yield e)する。Flatten操作。
    • コルーチンが入れ子構造になる場合、動作効率改善のため内部的に対称転送(symmetric transfer)が行われる。*6
  • co_await式はサポートしない。
  • コルーチンから送出された例外はジェネレータ利用側にそのまま伝搬する(→id:yohhoy:20211112)。
    • co_yield elements_of(child)の子コルーチンchildの送出例外は、親コルーチン側でもcatch可能。ハンドリングしなければ親コルーチンが返すジェネレータ利用側へと伝搬する。
  • アロケータサポート
    • コルーチンフレーム*7のメモリ確保/解放に用いられる。
    • テンプレートパラメータAllocator型またはコルーチン引数にてstd::allocator_arg_t+アロケータ型を指定する。
    • アロケータの型消去(type erasure)をサポート。例: std::generator<int>型は任意のアロケータ型をコルーチンフレーム上に格納する。
    • ステートレス/ステートフル両アロケータをサポート。
  • 動作性能はC++コンパイラの最適化性能に強く依存する(→id:yohhoy:20180415)。

関連URL

*1:std::ranges::fold_left アルゴリズムP2322R6 にてC++2b標準ライブラリへ追加予定となっている。

*2:std::generator<R>::promise_type 型の初期サスペンドポイント(initial_suspend)は suspend_always 型を返す。

*3:C++20 <coroutine>ヘッダはコルーチン機構実装用の低レイヤ機能のみを提供する。

*4:Value型は std::generator それ自身からは参照されず、利用側で イテレータvalue_type 型にアクセスするユースケース向け。

*5:テンプレートパラメータ R に対して "reference collapsing" 規則が適用される。例: R=const T& ならば R&&=const T& となる。

*6:std::generator<R>::promise_type 型の yield_value メンバ関数が返すAwaiter型では、await_suspend メンバ関数が転送先コルーチンの std::coroutine_handler 値を返す。

*7:C++標準規格では "coroutine state" と呼称される。

C++オブジェクト基本操作:Copyable/Movable/Swappable

プログラミング言語C++のオブジェクトに対する基本操作として、コピー可能(copyable)/ムーブ可能(movable)/交換可能(swappable)の3段階が存在する。

C++における最も原始的なオブジェクト操作は交換(swap)であり、ムーブ(move)、コピー(copy)の順にオブジェクト型に対する要件が拡張される関係にある。*1

  • 交換可能(swappable):2つの変数間で「値の入れ替え」を行える。(例:無効値表現を持たないオブジェクト*2
  • ムーブ可能(movable):交換可能かつ、別の変数へ「値の移動」を行える。(例:std::unique_ptr<T>
  • コピー可能(copyable):ムーブ可能かつ、別の変数へ「値の複製」を行える。(例:std::string

注:交換操作はムーブ操作を用いても実装可能だが、常にムーブ操作を必要とするわけではない(swapメンバ関数で十分)。

この3操作の関係性は、C++標準ライブラリ提供コンセプト定義における包摂関係(subsumption relation)としても表現されている(→id:yohhoy:20190903)。C++20 18.4.9/p4, 18.6/p1より一部引用。

template<class T>
  concept swappable = requires(T& a, T& b) { ranges::swap(a, b); };
// (snip)

[Note: The semantics of the swappable and swappable_with concepts are fully defined by the ranges::swap customization point. -- end note]

This subclause describes concepts that specify the basis of the value-oriented programming style on which the library is based.

template<class T>
  concept movable =
    is_object_v<T> && move_constructible<T> &&
    assignable_from<T&, T> && swappable<T>;

template<class T>
  concept copyable =
    copy_constructible<T> && movable<T> && assignable_from<T&, T&> &&
    assignable_from<T&, const T&> && assignable_from<T&, const T>;
// (snip)

関連URL

*1:ムーブ可能だが交換不可能な型、コピー可能だがムーブ不可能な型はC++のセマンティクス上存在しえない。

*2:C++標準ライブラリでは “交換可能だがムーブ不可能な型” は提供されない。例えばムーブコンストラクタ・代入演算子をdelete宣言かつ swap 非メンバ関数のみ提供する型が該当する。https://wandbox.org/permlink/3IebVhspiFU3kgQU

"Dreams come" == true

プログラミング言語PHPにおける奇妙な型変換 "Type Juggling"・第2弾。真偽値との比較にも要注意。

<?php
var_dump("Dreams come" == true);
// bool(true)

var_dump("Dreams come" === true);
// bool(false)
?>

Type Juggling規則により「片方がbool型の場合は両辺をbool型として比較評価」する。

関連URL

演算子オーバーロード for 日付リテラル

C++20標準ヘッダ <chrono> カレンダー(Calendar)ライブラリが提供する、日付リテラル表記用の/演算子オーバーロード一覧。

ノート:年月日順で日付リテラルを述する場合、少なくとも年(year)フィールドは常に型を明示した方がトラブルリスク*1が小さい。/演算子は左結合となることをお忘れなく。

#include <chrono>
using namespace std::chrono;

// 2022-06-13
auto date1 = 2022y/6/13;    // OK: year_month_day{year{2022}, month{6}, day{13}}
auto date2 = 2022/June/13;  // NG: ill-formed
auto date3 = 2022/6/13d;    // NG: month_day{month{337}, day{13}}
auto date4 = 2022/6/13;     // NG: int{25}

auto date2m = 2022/(June/13);  // OK: 意図通りに解釈されはするものの
auto date2d = 2022/(6/13d);    // OK: これらの難解表記は避けるべき…

年月日指定:*2

左辺 右辺 結果 表記
year month year_month 年/月 *
year int year_month 年/月 *
month day month_day 月/日 *
month int month_day 月/日 *
int day month_day 月/日 *
day month month_day 日/月
day int month_day 日/月
year_month day year_month_day 年+月/日 *
year_month int year_month_day 年+月/日 *
year month_day year_month_day 年/月+日 *
int month_day year_month_day 年/月+日 *
month_day year year_month_day 月+日/年
month_day int year_month_day 月+日/年

最終日指定:*3

左辺 右辺 結果 表記
month last_spec month_day_last 月/最終日 *
int last_spec month_day_last 月/最終日 *
last_spec month month_day_last 最終日/月
last_spec int month_day_last 最終日/月
year month_day_last year_month_day_last 年/月+最終日 *
int month_day_last year_month_day_last 年/月+最終日 *
month_day_last year year_month_day_last 月+最終日/年
month_day_last int year_month_day_last 月+最終日/年

第n曜日指定:

左辺 右辺 結果 表記
month weekday_indexed month_weekday 月/第n曜日 *
int weekday_indexed month_weekday 月/第n曜日 *
weekday_indexed month month_weekday 第n曜日/月
weekday_indexed int month_weekday 第n曜日/月
year_month weekday_indexed year_month_weekday 年+月/第n曜日 *
year month_weekday year_month_weekday 年/月+第n曜日 *
int month_weekday year_month_weekday 年/月+第n曜日 *
month_weekday year year_month_weekday 月+第n曜日/年
month_weekday int year_month_weekday 月+第n曜日/年

最終曜日指定:*4

左辺 右辺 結果 表記
month weekday_last month_weekday_last 月/最終曜日 *
int weekday_last month_weekday_last 月/最終曜日 *
weekday_last month month_weekday_last 最終曜日/月
weekday_last int month_weekday_last 最終曜日/月
year_month weekday_last year_month_weekday_last 年+月/最終曜日 *
year month_weekday_last year_month_weekday_last 年/月+最終曜日 *
int month_weekday_last year_month_weekday_last 年/月+最終曜日 *
month_weekday_last year year_month_weekday_last 月+最終曜日/年
month_weekday_last int year_month_weekday_last 月+最終曜日/年

関連URL

*1:date3 は std::chrome::month クラスの未規定(unspecified)仕様により、大半のC++処理系で month_day{month{81}, day{13}} として保持される可能性が高い。(C++20 27.8.4.1/p1, 27.8.4.2/p1)

*2:ISO 8601準拠の記述順(年-月-日)のみをサポートするならば、表中 * 印オーバーロードのみで十分だったはず。

*3:ある月の最終日表現には std::chrono::last 定数を用いる。

*4:ある月の最終曜日を std::chrono::weekday_last 型 = std::chrono::weekday 型 + std::chrono::last 定数で表現する。

*5:C++20のベースとなったCalendarライブラリに関するCppCon 2015プレゼンテーション