LoginSignup
147
98

競プロで役立つC++20新機能

Last updated at Posted at 2022-04-06

はじめに

競プロのコーディングが快適になるC++20新機能をまとめました!!

C++20の豊富な新機能から競プロで便利な機能を合計で16個紹介します.

※(2023/8/7追記) AtCoderでは2023年の言語アップデートにより、ほとんどの機能が使用可能となりました。新バージョンのgcc12.2では、紹介されている機能のうち <format> を除くすべての機能が使用可能です。

参考文献

https://cpprefjp.github.io/lang/cpp20.html
https://en.cppreference.com/w/cpp/20

を参考にしました.

cpprefjp以外をあまり見ていないので,間違っているところがあるかもしれません.
誤りに気づいたら指摘していただけると幸いです.

標準ライブラリの新機能

1. コンテナのメンバ関数の追加

1-1. 連想配列に contains メンバ関数を追加

連想配列のキーに値が存在するか判定するメンバ関数が追加されます.
今までの a.find(value) != a.end() という書き方より簡潔になります.

#include <iostream>
#include <map>
#include <unordered_set>

int main() {
    std::map<int, int> a{{1, 2}, {1, 3}, {3, 4}};
    std::unordered_multiset<int> b{1, 2, 3};

    std::cout << std::boolalpha;
    std::cout << a.contains(1) << '\n';
    std::cout << b.contains(4) << '\n';
    // true
    // false
}

1-2. 文字列型に starts_withends_with メンバ関数を追加

先頭や末尾が指定の文字列と一致するか調べることができます.

#include <iostream>
#include <string>

int main() {
    std::string s = "Hello world!";
    std::cout << std::boolalpha << s.starts_with("Hello") << '\n';
    // true
}

2. <numbers>

様々な数学定数が提供される,<numbers> ヘッダが追加されます.

ネイピア数や円周率といった数学定数が numbers 名前空間に定義されています.

#include <iostream>
#include <numbers>

int main() {
    // double型の数学定数
    std::cout << std::numbers::e << ' ' << std::numbers::pi << '\n';
    // 2.71828 3.14159

    // 型を指定することもできる
    std::cout << std::numbers::phi_v<long double> << '\n';
    // 1.61803
}

cpprefjp: https://cpprefjp.github.io/reference/numbers.html
cppreference: https://en.cppreference.com/w/cpp/header/numbers

3. <bit>

popcountなどのbit操作を提供する <bit> ヘッダが追加されます.
__builtin_popcount などを直接使うことなく高速なbit操作を使用できます.

競プロでよく使う操作を以下に挙げます:

  • std::bit_ceil:n以上の最小の2の冪乗を返す
  • std::bit_floor:n以下の最大の2の冪乗を返す
  • std::bit_width:bitの幅を返す
  • std::countl_zero:左側の連続した0の個数を返す
  • std::countl_one:左側の連続した1の個数を返す
  • std::countr_zero:右側の連続した0の個数を返す
  • std::countr_one:右側の連続した1の個数を返す
  • std::popcount:立っているbitの数を返す

符号なし整数型しか使えないことに注意が必要です(unsignedへのキャストを明示することになるのでわかりやすいです).

cpprefjp: https://cpprefjp.github.io/reference/bit.html
cppreference: https://en.cppreference.com/w/cpp/header/bit

4. <format>

※2023年現在AtCoderのgcc12.2では使用できません。

書式文字列に沿ってフォーマットする機能が <format> ヘッダに追加されます.
C言語の sprintf と比べて型安全で書きやすくなります.

#include <format>
#include <iostream>

int main() {
    int a = 1, b = 2;
    std::cout << std::format("a, b = {}, {}\n", a, b);
    std::cout << std::format("b, a = {1}, {0}\n", a, b); // 順序を指定
    // a, b = 1, 2
    // b, a = 2, 1
}

std::format の第一引数はコンパイル時に整合性が検査されるため,コンパイル時定数である必要があります.

詳しい書式はhttps://cpprefjp.github.io/reference/format/format.html に書いてあります.

cpprefjp: https://cpprefjp.github.io/reference/format.html
cppreference: https://en.cppreference.com/w/cpp/header/format

5. constexpr化

STL機能の大部分がconstexpr化され,コンパイル時計算で使用可能になります.

特に,C++20で動的メモリ確保がコンパイル時に行えるようになったので std::vectorstd::string がconstexpr化されています.
※ただし動的メモリを持ち越すことはできないため,constexpr関数の計算途中で使用することしかできません.

#include <iostream>
#include <numeric>
#include <vector>

constexpr int sum(const std::vector<int>& a) {
    return std::reduce(a.begin(), a.end());
}
    
int main() {
    constexpr int result = sum({1, 2, 3});
    std::cout << result << '\n';
    // 6
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/more_constexpr_containers.html

6. Range

6-1. Rangeのアルゴリズム関数

今までのC++では開始イテレータと終了イテレータのペアで範囲を表してきました.

C++20からはコンテナや配列,部分的なコンテナといった範囲(Range)を直接扱えるようになります.

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> a = {3, 2, 1}, b = {3, 2, 1};

    // 範囲を直接扱う
    std::ranges::sort(a);
    // 従来通り開始イテレータと終了イテレータのペアを渡すこともできる
    std::ranges::sort(b.begin(), b.end());
}

これにより今までは競プロでコード短縮に使用していた all マクロが不要になります.

<algorithm> ヘッダの ranges 名前空間では今までイテレータのペアで範囲を表してきたアルゴリズム関数が,Rangeを直接扱うアルゴリズム関数として提供されています.

std::ranges を省略する

競プロをする上では名前空間 std::ranges を省略したいです.しかし using namespace std::ranges; してしまうと,従来のアルゴリズム関数と名前が被ってしまいます.

そこで次のような裏技が使えます:

#include <bits/stdc++.h>

struct Hoge {};

template<>
struct std::ranges::view_interface<Hoge> {
    static void main() {
        // ここに処理を書く
        vector<int> a{1, 2, 3};
        // std::ranges::maxが呼ばれる
        cout << max(a) << '\n';
    }
};

int main() {
    std::ranges::view_interface<Hoge>::main();
}

処理を書く場所が std::ranges 名前空間下になるので,Rangeのアルゴリズム関数が優先的に呼び出されます.(std名前空間に関数を追加するのは未定義動作なので、STLクラスをテンプレート特殊化することで達成しています.)

6-2. view

viewとは,コンテナなどとは違ってメモリ上には存在しないが,Rangeとして扱えるクラスのことです.

例えば std::ranges::iota_view では,範囲 [l, r) を扱うことができます.rangeコンセプトの要件を満たしているため,範囲for文を使って範囲の整数を順次取り出すことができます.

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    // std::views::iotaはstd::ranges::iota_viewを生成するヘルパ関数
    for (auto i: std::views::iota(0, 100)) {
        std::cout << i << '\n';
    }
    
    // 以下と同じ
    // for (int i = 0; i < 100; ++i) {
    //     std::cout << i << '\n';
    // }

    // 整数だけでなくイテレータも入れられる
    std::vector<int> a = {1, 2, 3};
    for (auto i: std::views::iota(a.begin(), a.end())) {
        std::cout << i - a.begin() << ' ' << *i << '\n';
    }
}

std::views::iota は競プロで多用されている rep マクロの代わりになります.

他にも様々な関数が追加されています:

#include <iostream>
#include <map>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> a = {1, 2, 3};

    // 条件を満たす要素のみを返す
    for (auto i: std::views::filter(a, [](int i) { return i % 2 == 1; })) {
        std::cout << i << '\n';
    }
    // 1
    // 3

    // 全ての要素に関数を適用して返す
    for (auto i: std::views::transform(a, [](int i) { return i * 2; })) {
        std::cout << i << '\n';
    }
    // 2
    // 4
    // 6

    // 要素の連結を返す
    for (auto i: std::views::join(std::vector{a, a})) {
        std::cout << i << '\n';
    }
    // 1
    // 2
    // 3
    // 1
    // 2
    // 3

    // イテレータからn個先までの要素を返す
    for (auto i: std::views::counted(a.begin(), 2)) {
        std::cout << i << '\n';
    }
    // 1
    // 2

    // 連想配列のキーのみを取り出す
    // std::views::keys, std::views::valuesはそれぞれstd::views::elements<0>, std::views::elements<1>と同じ
    for (auto i: std::views::keys(std::map<int, int>{{1, 2}, {2, 3}})) {
        std::cout << i << '\n';
    }
    // 1
    // 2

    // 要素の逆順を返す
    for (auto i: std::views::reverse(a)) {
        std::cout << i << '\n';
    }
    // 3
    // 2
    // 1
}

このような多様な機能により,今までのイテレータを使った方法以上に汎用性の高い記述が可能になっています.

また関数がネストするのを防ぐために,operator | を利用したパイプライン記法も可能となっています.

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    // [0, 10)の各要素を2倍したものの逆順を返す
    for (auto i: std::views::iota(0, 10)
               | std::views::transform([](int i) { return i * 2; })
               | std::views::reverse) {
        std::cout << i << '\n';
    }
}

メソッドチェーンのように,実行される順序と記述する順序が一致していて見通しが良くなります.

cpprefjp: https://cpprefjp.github.io/reference/ranges.html
cppreference: https://en.cppreference.com/w/cpp/header/ranges

7. プロジェクション

例えば std::pairsecond の値をもとにソートをしたいときは頻繁にあるでしょう.

今までのC++では比較関数を書くしかないので,少し面倒でした.

#include <algorithm>
#include <vector>
#include <utility>

int main() {
    std::vector<std::pair<int, int>> a = {{1, 3}, {2, 2}, {3, 1}};

    // secondの値でソート
    std::sort(a.begin(), a.end(), [](auto a, auto b) { return a.second < b.second; });
}

しかし std::ranges::sort では,second の値を返す関数を渡すことで,簡潔にソートを記述することができます.

#include <algorithm>
#include <vector>
#include <utility>

int main() {
    std::vector<std::pair<int, int>> a = {{1, 3}, {2, 2}, {3, 1}};

    // secondの値でソート
    std::ranges::sort(a, {}, [](auto p) { return p.second; });

    // secondの値で降順ソート
    // 第二引数に比較関数を渡すと,第三引数の関数の返り値をもとに比較が行われる
    std::ranges::sort(a, std::ranges::greater{}, [](auto p) { return p.second; });
}

これはPythonの sort 関数の key などと同じ機能で,プロジェクションと呼ばれています.

さらに呼び出しに std::invoke を使っているため,second へのメンバポインタを渡すことでより簡潔に記述することもできます.

#include <algorithm>
#include <vector>
#include <utility>

int main() {
    std::vector<std::pair<int, int>> a = {{1, 3}, {2, 2}, {3, 1}};

    // secondの値でソート
    std::ranges::sort(a, {}, &std::pair<int, int>::second);
}

メンバポインタは構造体のメンバの内部位置を表すポインタなので,オブジェクトのメンバを取得することができます.

std::invoke では,通常の関数の呼び出しとメンバポインタを使ったメンバの取得を一般化して同じように扱うことができるので,このような記述ができます.

プロジェクションは std::ranges::lower_bound など,比較関数をとる他のRangeアルゴリズム関数にも追加されています.

新しい言語機能

1. auto の拡張

ジェネリックラムダの引数の auto 宣言と同様に,普通の関数でも auto で引数宣言をすることが可能になります.

#include <iostream>

// 関数では3つのautoが使える
template<auto x>
auto func(auto y) {
    return x + y;
}

int main() {
    std::cout << func<2>(3) << '\n';
    // 5
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/function_templates_with_auto_parameters.html

2. テンプレート推論の拡張

C++17で追加されたテンプレート推論が強化されます.

  • エイリアステンプレート(using)経由のテンプレート推論
  • 集成体初期化のテンプレート推論

が追加されます.

#include <memory>
#include <vector>

template<class T, class Alloc = std::allocator<T>>
using vec = std::vector<T, Alloc>;

template<class T, class U>
struct Pair {
    T a;
    U b;
};
// 以下の推論補助が不要
// template<class T, class U>
// Pair(T, U) -> Pair<T, U>;

int main() {
    vec a = {1, 2, 3}; // エイリアステンプレート
    Pair b = {1, 2.0}; // 集成体初期化
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/class_template_argument_deduction_for_alias_templates.html
https://cpprefjp.github.io/lang/cpp20/class_template_argument_deduction_for_aggregates.html

3. 非型テンプレートの拡張

今までは

  • 整数型
  • 左辺参照型
  • std::nullptr_t

の3つの型しか非型テンプレートに入れることができませんでした.

しかし,C++20では,以下の型に拡張されます.

  • スカラ型
  • 左辺参照型
  • 以下の特徴をもつリテラルクラス型
    • すべての基本クラスとメンバ変数がpublicかつmutableでない
    • すべての基本クラスとメンバ変数が構造的型もしくはその配列である

具体的には std::pair<double, double>std::array<int, 3>,ラムダといった型を使用することができるようになります.

また非型テンプレート内では,auto を使って宣言するだけでなく,テンプレート推論をすることができるようになります.

#include <iostream>
#include <utility>

template<std::pair p> // テンプレート推論
void show() {
    std::cout << p.first << ' ' << p.second << '\n';
}

int main() {
    show<{3.14, 'c'}>();
    // 3.14 c
}

文字列や配列を扱いたい場合,非型テンプレートの型を const char[] などにするとポインタとして扱われてしまうため,以下のような問題が発生します.

  • staticな値のポインタしか渡せないので,リテラルはそのまま渡せない
  • 型が一致するかはポインタの比較で調べられるため,同じ値でも別の型として扱われてしまう
#include <iostream>
#include <type_traits>

template<const char s[]>
struct Hoge {};

int main() {
    std::cout << std::boolalpha;

    // 文字列リテラルは渡せない
    // sとtは値が同じでもアドレスは違うので,別の型として扱われる
    static constexpr const char s[] = "Hello", t[] = "Hello";
    std::cout << std::is_same_v<Hoge<s>, Hoge<t>> << '\n';
    // false
}

そのため自作のクラスを用意すると便利です.

#include <cstddef>
#include <iostream>

template<std::size_t len>
struct MyString {
    const char str[len];
};

template<MyString s> // テンプレート推論
void show() {
    std::cout << s.str << '\n';
}

int main() {
    show<{"Hello world!"}>();
    // Hello world!
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/class_types_in_non-type_template_parameters.html

4. ラムダの拡張

4-1. テンプレートの使用

ラムダでテンプレートを使用することができるようになります.

#include <vector>

int main() {
    auto func = []<class T>(const std::vector<T>& a) {
        return a;
    };
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/familiar_template_syntax_for_generic_lambdas.html

4-2. キャプチャしていないラムダがデフォルト構築・代入可能に

キャプチャしていないラムダに限りデフォルトコンストラクタとコピー,ムーブ代入演算子が default 指定されるようになります.

これを活用すると,以下のように一行で std::set の比較関数を書くことができます.

#include <cmath>
#include <set>

int main() {
    // 絶対値が少ない順で並べるstd::set
    // C++17までは二行必要だった
    auto f = [](int a, int b) { return std::abs(a) < std::abs(b); };
    std::set<int, decltype(f)> s(f);

    // C++20からは評価されない文脈でのラムダも許可されるので,一行で書ける
    std::set<int, decltype([](int a, int b) { return std::abs(a) < std::abs(b); })> t;
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/default_constructible_and_assignable_stateless_lambdas.html

5. 丸括弧による集成体初期化

集成体初期化を丸括弧で行うことができるようになります.
C++11の一様初期化によりコンストラクタを波括弧で呼び出せるようになりましたが,その逆です.

集成体初期化をコンストラクタと同じように使えるだけなので,全てが丸括弧に置き換えられるわけではありません.

struct S {
    int x, y;
};

int main() {
    int a[](1, 2, 3);
    // これはコンパイルエラー
    // int a[] = (1, 2, 3);

    S b[]({1, 2}, {2, 3});
    // これもコンパイルエラー
    // S b[]((1, 2), (2, 3));
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/allow_initializing_aggregates_from_a_parenthesized_list_of_values.html

6. 指示付き初期化

C言語の機能追加に伴い,集成体初期化で変数を指定して初期化できるようになります.

struct S {
    int x, y, z;
};

int main() {
    S a{.x{1}, .y{2}, .z{3}};
    S b = {.x = 1, .y = 2, .z = 3};
}

ただしC++ではC言語と異なり,変数の順序が異なる場合コンパイルエラーとなります.

順序を入れ替えられないので少し扱いにくいですが,これを利用して簡単なキーワード引数が実現できます.

#include <cstddef>
#include <iostream>
#include <utility>

// print関数の制御に用いる
// sep: print関数の区切り文字列
// end: print関数の最後に出力する文字列
// flush: flushするかの真偽値
template<std::size_t sep_len = 2, std::size_t end_len = 2>
struct PrintKwArgs {
    const char sep[sep_len] = " ";
    const char end[end_len] = "\n";
    bool flush = false;
};

// print関数
template<PrintKwArgs kwargs = {}>
void print() {
    std::cout << kwargs.end;
    if constexpr (kwargs.flush) fflush(stdout);
}
template<PrintKwArgs kwargs = {}, class T>
void print(T&& a) {
    std::cout << a;
    print<kwargs>();
}
template<PrintKwArgs kwargs = {}, class T, class... Args>
void print(T&& a, Args&&... args) {
    std::cout << a << kwargs.sep;
    print<kwargs>(std::forward<Args>(args)...);
}

int main() {
    print(1, 2, 3);
    // 1 2 3
    print<{.sep = ", "}>(1, 2, 3);
    // 1, 2, 3
}

指示付き初期化でもテンプレート推論は効くようです.

cpprefjp: https://cpprefjp.github.io/lang/cpp20/designated_initialization.html

7. 初期化式を伴う範囲for文

以下のようなことができます:

#include <iostream>

int main() {
    // インデックスと値を取得
    for (int idx = 0; auto i: {1, 2, 3}) {
        std::cout << idx << ' ' << i << '\n';
        ++idx;
    }
    // 0 1
    // 1 2
    // 2 3
}

今までもif文やswitch文で初期化式を伴うことができましたが,範囲for文でも可能になります.

cpprefjp:https://cpprefjp.github.io/lang/cpp20/range-based_for_statements_with_initializer.html

8. 一貫比較

三方比較演算子/宇宙船演算子と呼ばれる,operator <=> が追加されます.

operator <=> を使用するには,<compare> ヘッダのインクルードが必須です(std::initializer_liststd::type_info と同様に,operator <=> の戻り値がSTLの型であるためです).

a <=> b はa < bならば0より小さい値を,a == bならば0と等しい値を,a > bならば0より大きい値を返します.

operator <=> の結果から他の比較演算子の結果が求まるので,operator <=> を定義すると他の比較演算子も暗黙定義されます.

さらに operator <=> は辞書順比較によるデフォルト実装があるので,辞書順比較で良い場合は実装を大幅に省略することができます.

#include <compare>
#include <iostream>
#include <vector>

struct Hoge {
    int n;
    std::vector<double> a;
    constexpr auto operator <=>(const Hoge&) const = default; // デフォルト実装に任せる
};

int main() {
    std::cout << std::boolalpha << (Hoge{1, {1, 2, 3}} < Hoge{1, {1, 3, 1}}) << '\n';
    // true
}

cpprefjp: https://cpprefjp.github.io/lang/cpp20/consistent_comparison.html

9. コンセプト

今までSFINAEというメタプログラミングで行ってきたものを言語機能として取り入れたものです.

requires式

requires式では,型の振る舞いを調べることができます.requires式の中に書かれた式や文がコンパイルエラーが出ずに評価できる場合はtrueが,評価できない場合はfalseが返ります.

#include <iostream>
int main() {
    // operator +(int, double)とoperator +(double, int)が正しく評価できるか調べる
    std::cout << std::boolalpha << requires(int x, double y) {
        x + y;
        y + x;
    } << '\n';
    // true
}

コンセプト

コンセプトは,型の振る舞いを調べたり振る舞いによってテンプレートを制約できる機能です.

定数式のbool型の変数テンプレートとほぼ同じように扱えます.

#include <iostream>

// x + y と y + x が正しく評価できるか調べるコンセプト
template<class T, class U>
concept Addable = requires(T& x, U& y) {
    x + y;
    y + x;
};

int main() {
    // コンセプトはbool型の変数テンプレートと同じように扱える
    std::cout << std::boolalpha << Addable<int, double> << '\n';
    // true
}

コンセプトによってテンプレートを制約する

さらにコンセプトを以下のような構文で使用することで,テンプレートを制約することができます.

#include <iostream>
#include <ranges>

// std::ranges::rangeは型がRangeであるか調べるコンセプト
// 構文1
template<std::ranges::range T>
void func1(T&& a) {
    for (auto&& i: a) std::cout << i << '\n';
}

// 構文2
void func2(std::ranges::range auto&& a) {
    for (auto&& i: a) std::cout << i << '\n';
}

// 構文3
template<class T>
requires std::ranges::range<T>
void func3(T&& a) {
    for (auto&& i: a) std::cout << i << '\n';
}

int main() {}

コンセプトで型を制約すると以下のような利点があります.

  • 制約を満たさない型を渡すと,内部の呼び出しを調べずに即コンパイルエラーとなるため,エラーメッセージが分かりやすくなる
  • 関数のオーバーロードやテンプレート特殊化では,制約を満たすかによって処理を分岐できる

テンプレート特殊化の例でRangeに対する std::hash の処理を特殊化してみます.

#include <cstddef>
#include <functional>
#include <ranges>
#include <unordered_set>
#include <vector>

template<std::ranges::range T>
struct std::hash<T> {
private:
    // Tの中身の型のstd::hash
    [[no_unique_address]] std::hash<std::ranges::range_value_t<T>> hasher;

public:
    // 凄そうなhash関数(https://suzulang.com/cpp-64bit-hash-combine/ より)
    constexpr std::size_t operator ()(const T& a) const {
        std::size_t seed = std::ranges::size(a);
        for (const auto& i: a) seed ^= hasher(i) + 0x9e3779b97f4a7c15LU + (seed << 12) + (seed >> 4);
        return seed;
    }
};

int main() {
    // 上でstd::hashを特殊化したことにより,std::vector<int>はstd::hashに対応している
    // std::unordered_setはstd::hashを使うため,特殊化が必要
    std::unordered_set<std::vector<int>> s = {
        {1, 2, 3},
        {2, 3, 4},
    };
}

上記の例のようなことを今までのSFINAEで行おうとしても,特殊化されていないと判定され,あまりうまくいきません.
しかしコンセプトでは型の制約が厳しくなっていることが明確になるので,きちんと特殊化されていると判定されます.

cpprefjp: https://cpprefjp.github.io/lang/cpp20/concepts.html

まとめ

C++20では非常に多くの機能追加があるためここに書ききれなかった内容もたくさんあります.

また,C++23ではさらに標準ライブラリのモジュール化,std::print,パターンマッチングなど競プロに便利な機能が提案されているようです.

C++20/C++23で,競プロコーディングはかなり変化するのではないかと思います.

最後まで読んでいただきありがとうございました.

147
98
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147
98