進捗置き場というわけでもない場所

プログラミングしてる中で見つけたこととか

1 ページで眺める C++17

この記事は C++AdC 2017 19 日目の記事です。

n 番煎じ過ぎてみなさんがやらなかったやつをやります。

色々と cpprefjp江添さんのやつ を参考にしています。記述が被っている場所すらあるかもしれません。ごめんなさい。

言語機能

コードでの説明が必要なやつから紹介。だんだん雑になるかも。

構造化束縛(structured bindings)

今まで std::tuple を扱うのは面倒で、例えば

std::tuple<int, double, std::string> tup = f(); // auto tup = f(); でも良い
auto x = std::get<0>(tup);
auto d = std::get<1>(tup);
auto s = std::get<2>(tup);

または

int x;
double d;
std::string s;
std::tie(x, d, s) = f();

みたいに書く必要があった。これは微妙に使う気が起きない。C++17 からはこう書ける

auto [x, d, s] = f();

これなら使う気がぐんぐん湧いてくる。

これは tuple に限った機能ではなく、配列、条件を満たすクラスなどにも使える

auto [a1, a2, a3] = { 5, 1, 3 };

struct Pos {
    int x = 10, y = 10, z = 10;
};
auto [x, y, z] = Pos();

便利だね。

ただ

int a = 5, b = 3;
[a, b] = std::minmax(a, b);

みたいなことは出来ない。あくまで構造化束縛は変数の定義/初期化のみの構文に留まっている。

こういう便利そうな機能が微妙な仕様で終わってるあたり C++ らしくていとあはれなり(社会性フィルター)。

条件式書くときの構文変更

書くたびになんか微妙な気がするこういうコード。

auto result = f();
if (result.isOK()) {
    // 処理
}

C++17 からはこう書ける

if (auto result = f(); result.isOK()) {
    // 処理
}

この構文は if だけでなく、whileswitch などでも使える。

template 引数推論

初期化時にちょっと嫌になるこういうやつ

std::vector<std::vector<int>> vec(m, std::vector<int>(n, x));

C++17 からはこう書ける

std::vector vec(m, std::vector<int>(n, x));

これは std::vector の template 引数が推論されるようになったから。

ある程度は自動で推論してくれるし、もし自動では出来ないような推論をさせたい場合

template <typename T>
struct Vec {
    std::vector<T> vec; // コンテナが渡されたとき、それを std::vector に持ち替えたい
    
    template <typename C>
    Vec(const C& rhv) : vec(rhv.begin(), rhv.end()) {}
};

template <typename C>
Vec(const C&) -> Vec<typename C::value_type>; // コンテナの value_type を T とする

のように Hoge(引数) -> Hoge<推論する型> という構文で推論させることができる。

これを利用して明示的に文字数を指定しないでコンパイル時文字列を扱えるようになったりするのを 組んでみた けど、割りと面白かった。

RVO の保証

今までこんなコードを書いた時、返り値のオブジェクトがコピーされる可能性があった。

std::vector<int> f() {
    auto c = hoge(); // 何かしらのコンテナが帰ってくる

    return std::vector<int>(c.begin(), c.end()); // ここで返り値のコピーが発生する可能性がある
}

なぜ「可能性」なのかというと、これをコピーしないようにする最適化、Return Value Optimization (RVO) が規格上認められていたからなのだが、C++17 からはこの関数 f の返り値を利用して値を初期化する場合。すなわち

auto v = f();

のようなユースケースの場合はこの最適化が強制されるようになった。

これにより、上記のコードで必ずコピーが発生しないようになる。

訂正: 記事公開時点では

std::vector<int> f() {
    std::vector<int> vec = hoge();
    
    // なんか処理

    return vec;
}

のようなコードで RVO が保証されるかのような書き方をしていましたがこれは誤りでした。関数の返り値が prvalue カテゴリでかつ変数の初期化に使われるときのみ RVO が保証されるようです。

ご指摘 していただいた銀天すばるさんに感謝。

template

template で任意の型のコンパイル時定数を渡したい人に朗報

template <auto X>
struct Hoge {
    // 色々
};

のようなコードを書くことで

Hoge<30> a;
Hoge<42ll> b;

void f() { std::cout << "Hello, world!" << std::endl; }
Hoge<f> c;

みたいなコードを書くことができる。局所的に役立ちそう。

constexpr if

あの C++ TMP を象徴する機能、SFINAE の地位を半分奪うかもしれない素晴らしい機能。

template <
    typename T, 
    std::enable_if_t<
        std::is_same_v<std::decay_t<T>, int>,
        std::nullptr_t
    > = nullptr
>
void f(T) {
    std::cout << "T is int" << std::endl;
}

template <
    typename T,
    std::enable_if_t<
        !std::is_same_v<std::decay_t<T>, int>
            && std::is_integral_v<T>,
        std::nullptr_t
    > = nullptr
>
void f(T) {
    std::cout << "T is integer but not int" << std::endl;
}

template <
    typename T,
    std::enable_if_t<
        !std::is_integral_v<T>,
        std::nullptr_t
    > = nullptr
>
void f(T) {
    std::cout << "T is not integer" << std::endl;
}

みたいなよくある地獄コード(実際はもうちょっとスマートに書けるかも)が、なんとこうなる。

template <typename T>
void f(T) {
    if constexpr (std::is_same_v<std::decay_t<T>, int>)
        std::cout << "T is int" << std::endl;
    else if constexpr (std::is_integral_v<T>)
        std::cout << "T is integer but not int" << std::endl;
    else
        std::cout << "T is not integer" << std::endl;
}

なんということでしょう。もうあのクソオーバーロード解決をしなくて良いのです。めでたしめでたし。

ところが一つ罠があり、この constexpr if の操作は Two Phase Lookup の二段階目で行われるため、パースした段階で明らかにエラーである

template <typename T>
void f() {
    if constexpr (false)
        static_assert(false);
    else
        std::cout << "fine" << std::endl;
}

とかのコンパイルが通らない。この解決策としてはコードを

template <typename T>
void f() {
    if constexpr (false)
        static_assert(std::is_integral_v<T> && false);
    else
        std::cout << "fine" << std::endl;
}

こうすると static_assert の中身が一応 T に依存するので、コンパイルが通るようになる。前からある問題とは言えとても C++ らしいエラーの出し方でいとあはれなり。

畳み込み演算(Fold Expression)

今までこう書いてたやつが

template <typename T>
auto sum(T x) {
    return x;
}

template <typename T, typename... Types>
auto sum(T x, Types... args) {
    return x + sum(args...);
}

こう書けるようになる

template <typename... Types>
auto sum(Types... args) {
    return (... + args);
}

これは (((args#0 + args#1) + args#2) + ...) みたいに展開される(表記は江添さんが使ってるやつです)。

(args#0 + (args#1 + (args#2 + ...))) みたいに展開したい場合

template <typename... Types>
auto sum(Types... args) {
    return (args + ...);
}

とすればいい。

注意点として、式全体を () でくくらないとエラーになる。

template <typename... Types>
auto sum(Types... args) {
    return ... + args; // 括弧がないのでエラー!
}

あと、畳み込む演算自体は結構自由にできる。こんなんでもいい。

template <typename... Types>
auto sum(Types... args) {
    return (... + f(args).hoge());
}

例では + 演算子を使っているが演算子ならだいたい使える。

余談: これを悪用してみたいなぁとか考える人向けの某中3女子さんのツイートがこちら

constexpr ラムダ

  • コードの説明ないけど重要そうだから上の方入れた
  • とうとう constexpr なラムダ式が作れるようになった
  • 特にキーワードも必要なく、単にコンパイル時に実行できそうなラムダはコンパイル時の文脈で使える
  • つまり constexpr な変数とか static_assert の条件式内でラムダ式が使える
  • 色々できそうだが、なんとSFINAEでの悪用を防ぐためにtemplate引数の中には書けないので注意

ラムダ式の *this キャプチャ

以下のコードには重大なバグがある

struct Hoge {
    int x;

    auto f() {
        return [this]() { return this->x++; };
    }
}

int main() {
    std::function<int()> f;
    {
        Hoge hoge{5};
        f = hoge.f();
        std::cout << f() << std::endl;
    }

    std::cout << f() << std::endl;
}

そう、最後のラムダ式内で this をキャプチャしたはいいものの、最後の f() の呼び出し時には hoge の寿命は切れている。つまりキャプチャしたはずの this の中身が開放されているので見事未定義動作を踏み抜くことになる。

これを回避するため C++17 では this の中身である *this を直接キャプチャできるようになった。

struct Hoge {
    int x;

    auto f() {
        return [*this]() { return this->x++; }; // *this がコピーキャプチャされるので寿命も安心
    }
}

int main() {
    std::function<int()> f;
    {
        Hoge hoge{5};
        f = hoge.f();
        std::cout << f() << std::endl;
    }

    std::cout << f() << std::endl;
}

ネストされた名前空間定義の省略

いままで書いていた

namespace A {
    namespace B {
        namespace C {
            // ここに色々書く
        }
    }
}

みたいな非常に微妙にアレなやつが

namespace A::B::C {
    // ここに色々書く
}

と書けるようになった。嬉しい。

属性の追加

[[fallthrough]][[nodiscard]][[maybe_unused]] が追加された。

[[fallthrough]] は switch 文の中で用いて

switch (hoge) {
    case A:
        // なんか処理
        [[fallthrough]]
    case B:
        // なんか処理
        break;
    default:
        break;
}

みたいにすると case A の最後の部分で警告が出なくなる。

[[nodiscard]]は関数の定義につけると、その関数の返り値が使われていないときに警告を出す。標準ライブラリでも malloc などいくつかの低レベルのメモリ操作関数にこの指定がついた。

[[maybe_unused]] は変数の定義につけるとその変数がその先使われていなくても警告を出さない。デバッグとかに便利かも。

一部演算子の評価順序の固定

int& f(int& x, int v) {
    std::cout << v;
    return x;
}

int x = 0;
f(x, 2) = f(x, 1);

このようなコードを書いたとき、今までは演算子の右辺左辺の評価順序が未規定だったのでこの結果は 12 にも 21 にもなり得た。

C++17 からは例えば b = a のとき a => b の順に評価されるされるのが保証された。結果さっきのコードの実行結果は必ず 12 になる。

このようにして評価順序が固定される演算一覧はこんな感じ。全部 a => b の順に評価される。

a.b
a->b
a->*b
b = a
b @= a // @ は任意の演算子
a[b]
a << b
a >> b

なお期待されていた f(a, b, c) などの関数呼び出しにおける引数 a, b, c の評価順序の固定はされなかった。未だに未規定です。

その他

コードで説明する感じでもない色々を箇条書きしていく。

noexcept 指定が関数の型の一部に

  • 今まで void f(int x)void g(int x) noexcept は同じ void(int) 型だった
  • C++17 からはこれが区別され、上の例でいえば g の型は void(int) noexcept になる
  • void(int) noexcept から void(int) には暗黙の変換がされるが、逆はされない
  • template 周りで既存のコードが動かなくなる可能性があるので注意
  • こんなところに入れてるけど結構重要な変更

byte 型の追加

  • 他の整数からの暗黙変換が不可能な 8bit 整数型、byte 型が追加された
  • ストレージ用の型なのか、四則演算が定義されていない

inline 変数

  • inline Hoge x; とかで inline な変数が作れるようになった
  • ヘッダーオンリーライブラリの幅が広がるね

static_assert の記法追加

  • static_assert(cond, "")static_assert(cond) と書けるようになった

using の可変長引数の展開が可能に

  • using Args::operator()...; みたいな事ができます
  • というか今までできなかったのに気づいたときは驚いた

UTF-8 文字リテラル

  • u8'A' で UTF-8 での A を表現できる
  • ただし、1 バイトの文字しか扱えないため u8'あ' などはエラー
  • なぜ char8_t が入らないのにこんなものを入れたのか

16 進数の浮動小数点リテラル

  • 0x0.01p0 => 1/256 = 0.00390625 みたいな感じ。p 以下は 10 進数で指数部を指定する

属性名前空間を using できるように

  • [[using ns : foo, bar]] int x; みたいにすると [[ns::foo, ns::bar]] int x; と同じ動作になる

トライグラフの削除

ライブラリ

全体的に触れるだけにします。ファイルシステムに関しては説明を放棄しています。

あと並び順はあまり重要度と関係がないため、実は重要な事が下に小さく書かれてたりするかも。

std::string_view

今まである文字列の部分文字列を取得したいときには substr を使って次のようにしていた。

std::string str = "kitty on your lap.";

std::cout << str.substr(2, 3) << std::endl; // => tty

これは便利ではあるが、一方で substr の返り値が std::string であるために、その生成を生成するための文字列コピーが走るというパフォーマンス的な問題が生じていた。

要はただ文字列の一部を参照するためだけにそのコピーを作るのは無駄という話。

というわけで文字列の参照のみを持つ std::string_view というのが入った。<string_view> を include することで使用可能になる。

これを使うとさっきのコードも

#include <string_view>

//...

std::cout << std::string_view(str).substr(2, 3) << std::endl;

こうすれば無駄なコピーが走ることはない。めでたしめでたし。

ただし、string_view が参照している文字列を書き換えることは出来ない。つまり以下のコードは通らない

auto sv = std::string_view(str);

sv[0] = 'a'; // エラー!書き換え不可能

std::optional

無効かもしれない値を表現するクラス、std::optional ができた。<optional> を include すると使える。

以下のようにして使う

#include <optional>

// ...

std::optional<int> f(int x) {
    int* res = hoge::proc(x); // ポインタを返す外部の関数
    if (res)
        return *res;
    else
        return std::nullopt;
}

以下のような事もできる

std::optional<int> opt = f();

std::cout << opt.value_or(42) << std::endl; // nullopt なら 42 として扱う

ようやく入った。しかし C++ なので opt.map(f) みたいなやつはない。

std::variant & std::visit

プログラムを書いているとたまに union を使いたい場面が出て来る。でも union は型の概念を破棄しているに等しいクソだった。

C++17 ではまともな union にあたる std::variant が追加された。これも <variant> ヘッダを include すると使える。

std::variant<int, double, std::string> var;
var = 42;
var = 5.0;
var = std::string("kitty on your lap");

var = 'a'; // これはエラー

値を扱うにはこうする

if (auto p = std::get_if<int>(var))
    std:cout << *p << std::endl;
else if (auto p = std::get_if<int>(var))
    std:cout << *p << std::endl;
else if (auto p = std::get_if<int>(var))
    std:cout << *p << std::endl;

しかしこれは流石に面倒なので std::visit() という関数が用意されている。

std::visit([](auto&& x) { std::cout << x << std::endl; }, var);

神的に便利になる。型で処理をスイッチしたい場合はそういうクロージャーを作ってそのインスタンスを渡してやれば OK。便利 (大事なことなので 2 回言いました)

std::any

上の 2 つは適当に機能を制限していたが、それをぶっちぎってなんでも入る型 std::any が追加された。<any> を include すると使える。

使い方は std::variant のもっと自由な感じなので省略。ただし値を取り出すときには std::any_cast() を使う必要がある。

初学者であるほど使いたがりそうだが、初学者であるほど使ってはいけない機能個人的 No.1。本当に必要な時に伝家の宝刀として。

ファイルシステムを扱う機能が追加

ファイルシステムを扱う機能群としてとうとう <filesystem> ヘッダが入った。また、この機能群はほぼ全て std::filesystem 名前空間上に定義される。

これに関しては 江添さんのやつ が非常に詳しい。というか僕には手に負えないので重要ではあるが存在の紹介にとどめさせていただきたい。

std::apply

std::visit() の tuple 版っぽい std::apply() が追加された。これで tuple の各要素に対する一様な操作が以下のように書ける。

std::tuple<int, double, std::string> tup = f();

std::apply([](auto&& x) { std::cout << x << std::endl; }, tup);

便利。

std::make_from_tuple

tuple の各要素をある型 T のコンストラクタに突っ込みたいときがあるが、今まではいちいち補助関数を書く必要があった。

面倒なので std::make_from_tuple() が追加された。

struct Hoge {
    Hoge(int, double, std::string); // 多変数を取るコンストラクタ
};

std::tuple<int, double, std::string> tup = f();

Hoge hoge = std::make_from_tuple<Hoge>(tup); // こんな感じ

連想配列系コンテナ関連

ノード単体を扱う機能がついた

  • std::mapstd::unordered_mapstd::multimap や対応する set 系コンテナに、その要素のノードを扱う型である node_type が追加された。
  • これは一見 iterator のように見えるが遷移操作ができず、単に要素自体を表す型になる。
  • これに合わせて、引数に渡した要素をコンテナから破棄してその node_type を取得する extract() メンバと、2 つのコンテナを連結させる merge() メンバが追加された

その他のメンバ追加

  • try_emplace()insert_or_assign() が追加された
  • try_emplace()emplace() と違って要素が新たに挿入されなくても引数に渡した変数がムーブされない
  • insert_or_assign()operator [] とほぼ同じだが、返り値が (要素の参照, 要素が追加されたかどうか) の pair になっている

<functional> 関連の色々

検索アルゴリズムの追加

  • <functional> ヘッダに検索アルゴリズムがいくつか追加された。
  • ボイヤー・ムーア法とかがある

std::not_fn

  • 「bool に変換できる型を返す関数の返り値を反転させる関数」を返す std::not_fn() が追加された
  • 名前が楽しくなさそうという印象が強い
  • 合わせて std::not1()std::not2()std::unary_negate() などは非推奨化された

std::invoke

  • 「関数を呼び出す関数」である std::invoke() が追加された
  • 嬉しい人には嬉しい

<type_traits> 関連の色々

std::void_t

  • テンプレート引数に何を渡しても void になる型、std::void_t が入った
  • 一見意味がなさそうに見えるが、SFINAE で便利になる。

変数テンプレートの追加

  • std::is_hoge<T>::value とか std::is_hoge<T>{} の代わりに std::is_hoge_v<T> と書けるようになった

std::invoke_result への移行

  • std::invoke_result<F, Args...> で、型 F の関数を型が Args... の引数で呼び出したときの返り値の型が取得できる
  • 元々 std::result_of があったが、ちょこちょこ問題があったためこれは非推奨化され、代わりに std::invoke_result が出来た

std::bool_constant

  • コンパイル時の型として区別できる bool 定数を表現するための std::bool_constant が入った
  • 今までなかったのが驚き

論理演算系の追加

  • 否定、論理積、論理和 などが入った
  • 例えば論理積なら std::conjunction<std::is_foo<T>, std::is_bar<T>>::value みたいな感じで書ける

その他にもなんか色々ある

メモリ系の色々

std::shared_ptr が配列に対応

  • タイトル通り。なぜいままでなかったのか

shared_ptr::weak_type

  • std::shared_ptr<T> のメンバ型に std::weak_ptr<T> に対するエイリアス、shared_ptr::weak_type が入った

memory_resource

  • メモリの確保/解放に関する新しい管理機能群が入ったヘッダ <memory_resource> が出来た
  • 要は次世代版アロケータ

メモリ管理機能機能の追加

  • <memory> に色々入った

キャッシュライン定数が追加

  • <new> ヘッダにキャッシュ効率を色々頑張りたい時用のコンパイル時定数、std::hardware_destructive_interference_sizestd::hardware_constructive_interference_size が入った

その他

重要なものも混ざっています

並列アルゴリズム

  • みんな大好き(?) <algoriothm> ヘッダの関数の並列化バージョンが入った
  • std::sort(std::execution::par, std::begin(vec), std::end(vec)) みたいにすると使える

コンテナに関するフリー関数の追加

  • <iterator> ヘッダに std::size()std::empty()std::data() が追加された
  • それぞれ(その機能が提供されていれば)配列を含めるコンテナの要素数、コンテナが空かどうか、要素の配列の先頭のポインタを取得する

std::scoped_lock

  • 今まで複数の変数を lock_guard するのは名前付けとかが面倒だった
  • C++17 では std::scoped_lock locks(x, y, z); みたいにすることで x, y, z 全てに対する lock_guard を取得できるようになった

std::shared_mutex

  • 書き込みが少なくて、読み込みが多い変数の mutex を効率的に行う shared_mutex が追加された
  • flag とかに使えそう(?)

std::as_const

  • const でない変数に const を一時的に付与したい時が割りとあるが、const auto& cx = x みたいにするか const_cast をするしかなかった
  • 不便なので <utility>std::as_const() が入った

std::clamp

  • <algorithm> ヘッダにある値を一定範囲に丸める関数、std::clamp() が入った
  • std::clamp(val, min, max) で値が [min, max] の範囲に入るよう丸められる
  • 数だけでなく一般に使えるので <cmath> じゃないっぽい

<cmath> ヘッダに色々入った

  • ベータ関数だのルジャンドル多項式だののすごい数学関数がいっぱい入った
  • 三次元版の std::hypot() とかも入った

std::gcd() & std::lcm()

  • std::gcd()std::lcm() が入った
  • <cmath> ヘッダではなく <numeric> ヘッダなので注意
  • 僕は性根が腐っているので早くこれに気付かずに死んでいく using namespace stder の顔が見たい

emplace 系関数の返り値が付いた

  • 今まで vector::emplace_back などの emplace 系関数の返り値は void だった
  • C++17 からはこれが挿入された要素の参照を返すように
  • つまり auto& back = vec.emplace_back(5); みたいな事が出来る

std::uncaught_exceptions

  • まだキャッチされていない例外の個数を取得する std::uncaught_exceptions が入った
  • 同時に std::uncaught_exception は非推奨になった
  • 名前の区別がつきにくくて困る

その他の非推奨になったもの

  • std::iterator
  • shared_ptr::unique()
  • <codecvt> ヘッダ
  • std::allocator の幾つかのメンバ

以上

どうでしょうか。色々抜けはあるかもしれませんが大体こんな感じだと思います。

個人的にはまあダメなところを上げれば concept がないだとか range がないだとか structured bindings が微妙だとか色々いえますけど、良く言えば「次に繋がるアップデート」になるかなと思います。

ただ地味に重大な問題として未だに C++17 を完全に実装したコンパイラが存在しない気がします。

というわけでみなさんも良き C++ ライフを。明日は I さんの「ネタはあるんですが,時間的余裕がない可能性が高く,万が一書けない場合は @azaika_ が代わりに」です!!!