最終更新日時(UTC):
が更新

履歴 編集

状態を持たないラムダ式を、デフォルト構築可能、代入可能とする(C++20)

概要

C++17まではラムダ式の生成するクロージャ型に定義される特殊メンバ関数はコピー/ムーブコンストラクタとデストラクタのみだったが、C++20からは状態を持たない、すなわちキャプチャをしていないラムダ式の場合にのみそのクロージャ型はデフォルトコンストラクタと代入演算子を持つようになる。

auto l = [](){ return 20;};
using lambda_t = decltype(l);

lambda_t l2{};  // ok、デフォルト構築

l2 = l;             // ok、コピー代入
l2 = std::move(l);  // ok、ムーブ代入


auto lc = [n = 1](){ return n;};
using clambda_t = decltype(lc);

clambda_t lc2{};    // ng、デフォルトコンストラクタはdeleteされている
clambda_t lc3{lc};  // ok、コピー構築

// 共にng、コピー代入演算子はdeleteされている
lc3 = lc;
lc3 = std::move(lc);

仕様

ラムダ式がキャプチャをしていない場合、そのクロージャ型にはデフォルトコンストラクタとコピー/ムーブ代入演算子がdefault指定で定義される。

ラムダ式がキャプチャをしている場合、そのクロージャ型のデフォルトコンストラクタとコピー代入演算子はdelete指定で定義される(ムーブ代入演算子は宣言されない)。

// キャプチャをしていないラムダ式のクロージャ型は次のようなメンバを持つ
struct closure {
  closure() = default;  // C++20から
  closure(const closure&) = default;
  closure(closure&&) = default;

  closure& operator=(const closure&) = default; // C++20から
  closure& operator=(closure&&) = default;      // C++20から

  auto operator()(params...) const;
};

// キャプチャをしているラムダ式のクロージャ型は次のようなメンバを持つ
struct closure {
  closure() = delete;
  closure(const closure&) = default;
  closure(closure&&) = default;

  closure& operator=(const closure&) = delete;

  auto operator()(params...) const;

  // コピーキャプチャした変数に対応するメンバ変数
  T1 c1;
  T2 c2;
  ...
};

以下の例では、「評価されない文脈でのラムダ式」による仕様を同時に用いている。

連想コンテナの比較をカスタマイズする

#include <iostream>
#include <set>

int main()
{
  std::set<int, decltype([](auto lhs, auto rhs){ return lhs > rhs;})> set{};
  set.insert({1, 2, 4, 3, 0, 10, 9, 7, 5, 6, 8, 1, 5, 10});

  for (auto n : set) {
    std::cout << n << "\n";
  }
}

出力

10
9
8
7
6
5
4
3
2
1
0

std::unique_ptrにカスタムデリータを設定する

#include <iostream>
#include <memory>

using handle_t = void*;

void close_handle(handle_t*) {
  std::cout << "The handle was closed" << std::endl;
}

handle_t get_handle() {
  static int n{};
  return &n;
}

int main()
{
  handle_t h = get_handle();
  std::unique_ptr<handle_t, decltype([](auto h) {close_handle(h);})> handle{&h};
}

出力

The handle was closed

この機能が必要になった背景・経緯

これまでは、上記の例のように動作をカスタムする関数オブジェクトをテンプレートパラメータで受け取る設計のクラスをカスタマイズするためにラムダ式を用いる場合、そのラムダ式のクロージャオブジェクトをコンストラクタに引き渡す必要があった。

// ラムダ式を一度変数に受けてから、コンストラクタに渡す
auto greater = [](auto lhs, auto rhs){ return lhs > rhs;};
std::set<int, decltype(greater)> set{greater};

auto custom_deleater = [](auto h) {close_handle(h);};
std::unique_ptr<handle_t, decltype(custom_deleater)> handle{&h, custom_deleater};

これらのクラスは渡された関数オブジェクトの型がデフォルト構築可能であれば内部でデフォルト構築して初期化を行うため、本来はこのようにコンストラクタから渡す必要はない。しかし、C++17までのラムダ式はデフォルトコンストラクタが常にdeleteされていたためそれができなかった。

また、そのように構築したオブジェクトを代入しようとする場合、ラムダ式のクロージャ型の代入演算子が常にdeleteされていたためできなかった。

auto greater = [](auto lhs, auto rhs){ return lhs > rhs;};
std::set<int, decltype(greater)> set1{greater}, set2{greater};

set1 = set2;  // C++17まではコンパイルエラー

ラムダ式は関数オブジェクトを生成する糖衣構文であるが、これらのことは対応する関数オブジェクトを直接書いた場合と一貫しておらず不便であるため、状態を持たないラムダ式に限ってデフォルト構築と代入が可能になるように変更された。

C++20では同時に評価されない文脈にラムダ式を書くことができるようになったため、この様な場合にラムダ式のクロージャ型を直接取得し渡すことができ、余計なコードをほとんど削減することができるようになる。

関連項目

参照