C++17

Takami Torao C++17 #CPP #CPP17
  • このエントリーをはてなブックマークに追加

はじめに

Table of Contents

  1. はじめに
  2. コア言語機能
    1. 構造化バインディング
      1. コピーバインディング
      2. 参照バインディング
      3. Tuple-Like API の提供
    2. if - switch 文での初期化
    3. インライン変数
  3. テンプレートとメタプログラミング
    1. クラステンプレート引数推論
    2. コンパイル時 if
    3. 畳み込み式
  4. 新属性と構文改善
  5. 標準ライブラリの強化
    1. std::optional<T>
    2. オプション
    3. 文字列ビューとバイト列
    4. ファイルシステムライブラリ
    5. 並列アルゴリズム
    6. 追加ユーティリティ
  6. メモリと並列処理
  7. 参考文献

コア言語機能

C++17 では言語の基本構文に対してボイラープレートやスコープ管理の煩雑さを軽減する小規模な改善が施されている。

構造化バインディング

構造化バインディング (structured bindings) は構造体やタプル、配列のメンバーを個別の変数に展開するための言語機能。多値返却 (multiple value return) をサポートすることでコードの可読性と保守性を向上させることを目的としている。

C++17 より前は std::pairstd::tuple を返して呼び出し側で .firststd::get<>() といった意味の薄い識別子を使って値を参照していた。これは可読性を低下させ、さらにネストした型では記述が複雑になりがちだった。

// g++ -std=c++17 -Wall -O2 -o structured-bindings structured-bindings.cpp
#include <tuple>
#include <iostream>
#include <string>

struct Employee { int id; std::string name; double salary; };
Employee getEmployee() {
  return { 1, "John",  2778.57 };
}

std::pair<int, int> getPoint() {
  return { 1, 2 };
}

int main() {
  // 構造体のメンバーを展開
  auto [ id, name, salary ] = getEmployee();
  std::cout << "Employee(" << id << ", " << name << ", " << salary << ")" << std::endl;  // Employee(1, John, 2778.57)

  // pair や tuple を展開
  auto [ x, y ] = getPoint();
  std::cout << "Point(" << x << ", " << y << ")" << std::endl;  // Point(1, 2)

  // 静的にサイズが分っている配列も展開可能
  int array[3] = { 10, 20, 30 };
  auto [ a, b, c ] = array;
  std::cout << "Array(" << a << ", " << b << ", " << c << ")" << std::endl; // Array(10, 20, 30)

  return 0;
}

構造化バインディングに類似した機能は他のプログラミング言語でもサポートされている。例えば Python ではアンパック (unpack)、JavaScript は分割代入 (destructuring assignment) が該当する。Rust や Scala の抽出子 (extractor) も似ているが、これらは多値展開が目的というより実行時のパターンマッチの結果として機能する点がやや異なる。

コピーバインディング

構造化バインディングは、オブジェクトのコピーを匿名の変数にコピーし、各メンバーのエイリアスとして変数を定義するように機能する。つまり以下の左の構造化バインディングはコンパイラ内部では概ね右のような擬似コードに展開される。

auto [ x, y ] = getPoint();
auto __sb = getPoint();
auto x = __sb.x;
auto y = __sb.y;
// 展開されたフィールドはコピーなので変更しても元の構造体は影響を受けない
struct Employee { int id; std::string name; double salary; };
auto e = Employee { 1, "John",  2778.57 };
auto [ id, name, salary ] = e;
name = "Carol";
std::cout << "Employee(" << e.id << ", " << e.name << ", " << e.salary << ")" << std::endl;  // Employee(1, John, 2778.57)

参照バインディング

参照型の構造化バインディングでは、展開された変数は元のオブジェクトの参照として定義するように機能する。したがって変数の値を変更することで元のオブジェクトのフィールドを更新することができる。

auto p = Point { 1, 2 };
auto& [ x, y ] = p;
auto p = Point { 1, 2 };
const auto& __sb = p;
const auto& x = __sb.x;
const auto& y = __sb.y;
// 展開されたフィールドは参照なので変更は元の構造体を更新する
struct Employee { int id; std::string name; double salary; };
auto e = Employee { 1, "John",  2778.57 };
auto& [ id, name, salary ] = e;
name = "Carol";
std::cout << "Employee(" << e.id << ", " << e.name << ", " << e.salary << ")" << std::endl;  // Employee(1, Carol, 2778.57)

一時オブジェクトに対して参照バインディングが行われた場合、(言語仕様に基づき) その一時オブジェクトの寿命はバインディングされた変数と同じスコープまで自動的に延長される。したがって参照がダングリングポインターになることはない。ただし、一時オブジェクトを更新することはできないため const 修飾子を付ける必要がある。

const auto& [ x, y ] = getPoint();
const auto& __sb = getPoint();
const auto& x = __sb.x;
const auto& y = __sb.y;
// 一時オブジェクトの寿命は変数スコープまで延長されるが const 参照のみ許される
struct Employee { int id; std::string name; double salary; };
const auto& [ id, name, salary ] = Employee { 1, "John",  2778.57 };
std::cout << "Employee(" << id << ", " << name << ", " << salary << ")" << std::endl;  // Employee(1, John, 2778.57)

Tuple-Like API の提供

構造化バインディングは標準で std::pair, std::tuple, std::array をサポートするが、以下の 3 つの機能を提供すれば任意のユーザ定義型 T をサポートすることができる。

  1. メンバ数の定義。
    template<> struct tuple_size<T> {
      static constexpr std::size_t value = N;  // 型 T でアクセス可能なメンバ数
    };
  2. 各メンバの型の定義。
    template<> struct tuple_element<0, T> { using type = ElementType; };
    // ... 同様に 1, 2, ... のメンバに対する型を定義
  3. 各メンバを取得するための get<Idx>() オーバーロード。
    template<std::size_t Idx> auto get(const T& t);
    template<> auto get<0>(const T& t){ return ...; }
    // ... 同様に 1, 2, ... のメンバに対する取得関数を定義

以下の Customer 型はコンパイル時に構造化バインディングに失敗するが:

// g++ -std=c++17 -Wall -O2
#include <iostream>
#include <string>

class Customer {
private:
  long id;
  std::string name;
  std::string address;
public:
  Customer(long id, std::string name, std::string address): id(id), name(name), address(address) {}
  long getId() const { return id; }
  std::string getName() const { return name; }
  std::string getAddress() const { return address; }
};

int main() {
  Customer c(10, "Bola", "NY");
  auto [ id, name, address ] = c;
  std::cout << "Customer(" << id << ", " << name << ", " << address << ")" << std::endl;
  return 0;
}
  sb2.cpp: In function ‘int main()’:
sb2.cpp:20:8: error: cannot decompose inaccessible member ‘Customer::id’ of ‘Customer’
   20 |   auto [ id, name, address ] = c;
      |        ^~~~~~~~~~~~~~~~~~~~~
sb2.cpp:7:8: note: declared private here
    7 |   long id;
      |        ^~

追加で Customer 型に対する Tuple-Like API を定義すると構造化バインディングが可能になる。

#include <utility>
template<> struct std::tuple_size<Customer> { static constexpr int value = 3; };
template<> struct std::tuple_element<0, Customer> { using type = long; };
template<std::size_t Idx> struct std::tuple_element<Idx, Customer> { using type = std::string; };
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c){ return c.getId(); }
template<> auto get<1>(const Customer& c){ return c.getName(); }
template<> auto get<2>(const Customer& c){ return c.getAddress(); }

if - switch 文での初期化

if および switch では文頭での初期化文をサポートし、変数のスコープを制御構造内に限定することができる。これによって変数のスコープを不要に伸ばすことなく、RAII やパターンマッチ風のコードを簡潔に記述できるようになる。

#include <iostream>
#include <map>
#include <filesystem>

int main() {
  std::map<std::string, int> map;

  // if 冒頭の初期化; it は if スコープ内に限定される
  if(auto it = map.find("key"); it != map.end()) {
    std::cout << "found: " << it->second << std::endl;
  } else {
    std::cout << "not found" << std::endl;
  }

  // switch 冒頭の初期化
  namespace fs = std::filesystem;
  switch(fs::path path { "example.txt" }; fs::status(path).type()) {
    case fs::file_type::not_found:
      std::cout << path << " not found" << std::endl;
      break;
    case fs::file_type::directory:
      std::cout << path << " is directory" << std::endl;
      break;
    default:
      break;
  }

  return 0;
}

これは Go 言語で if 初期化文; 条件式 { ... } という形で変数のスコープを制御構造内に限定する構文と同じである。ただし、Go 言語ではエラー値を扱うことが主用途だが、C++ では任意の型を初期化でき、またスコープ終了時に自動的にデストラクタが呼ばれる点はファイルストリームやロックガードなどの RAII として使用することができる。

インライン変数

C++17 より前は、グローバル変数やクラスの静的データメンバの定義をヘッダーファイルに記述すると、複数の翻訳単位で同じ定義が現われることで ODR (One Definition Rule) 違反によるリンクエラーとなっていた。このため変数定義をソースファイル上のどこかの一カ所に記述し、ヘッダには extern 宣言のみを記述する必要があった。

C++17 ではこれらの変数を inline で定義することで、ヘッダの共有などにより複数の定義箇所があっても「唯一のオブジェクト」を複数の翻訳単位で安全に共有できる。これにより静的変数を使用するヘッダのみのライブラリを作成できるようになる。

// inlinevar1.cpp
#include "inlinevar.hpp"
#include <iostream>

int main() {
  add();

  std::cout << "counter: " << counter << std::endl;   // counter: 1
  return 0;
}
// inlinevar.hpp
#pragma once

inline int counter { 0 };

extern void add();
// inlinevar2.cpp
#include "inlinevar.hpp"

void add() { counter ++; }

上記の例で counter の宣言から inline を削除すると「counter が複数回定義されている」というリンカーエラーが発生する。

$ g++ -std=c++17 -Wall -O2 -o inlinevar inlinevar1.cpp inlinevar2.cpp && ./inlinevar
/usr/bin/ld: /tmp/ccdwKjhy.o:(.bss+0x0): multiple definition of `counter'; /tmp/ccaqyNEm.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

テンプレートとメタプログラミング

クラステンプレート引数推論

クラステンプレート引数推論 (class template argument deduction) はコンストラクタの引数からコンパイラがテンプレート引数を自動で推論する言語機能。C++17 より前はクラステンプレートを使うときに必ずすべてのテンプレート引数を明示的に指定しなければならなかったが:

std::complex<double> c { 5.1, 3.3 };
std::lock_guard<std::mutex> lg { mx };
std::vector<int> v { 1, 2, 3 };

これらはコンストラクタの引数から推論できるようになった。

std::complex c { 5.1, 3.3 };
std::lock_guard lg { mx };
std::vector v { 1, 2, 3 };

コンパイル時 if

if constexpr はコンパイル時に条件を評価して不要な分岐の処理を切り捨てる機能である。テンプレート内で使用することで、パフォーマンスの劣化なしに可読性を維持したまま、1 つの関数に記述した型ごとの分岐を最適化することができる。

条件式はコンパイル時定数であり、かつ、すべての分岐ブロックのコードは文法的に正しくなければならない。

// g++ -std=c++17 -Wall -O2 -o if-constexpr if-constexpr.cpp && ./if-constexpr
#include <string>
#include <cassert>

template<typename T> std::string as_string(const T& x) {
  if constexpr (std::is_arithmetic_v<T>) {
    // T が整数または浮動小数点の場合
    return std::to_string(x);
  } else if constexpr (std::is_same_v<T, std::string>) {
    // T が std::string の場合
    return x;
  } else {
    // それ以外の場合は T から std::string への返還を試みる
    return std::string{ x };
  }
}

int main() {
  assert(as_string(123) == "123");
  assert(as_string("hello") == "hello");
}

同等のコードは if constexpr の代わりにタグディスパッチを使用しても実現できるが、関数の分割と型特性メタプログラミングがやや冗長になる。

// g++ -std=c++17 -Wall -O2 -o tag-dispatch tag-dispatch.cpp && ./tag-dispatch
#include <string>
#include <cassert>

template<typename T> std::string _as_string_string(const T& x, std::true_type) {
  return x;
}
template<typename T> std::string _as_string_string(const T& x, std::false_type) {
  return std::string{ x };
}
template<typename T> std::string _as_string_arithmetic(const T& x, std::true_type) {
  return std::to_string(x);
}
template<typename T> std::string _as_string_arithmetic(const T& x, std::false_type) {
  return _as_string_string(x, std::is_same<T, std::string>{});
}
template<typename T> std::string as_string(const T& x) {
  return _as_string_arithmetic(x, std::is_arithmetic<T>{});
}

int main() {
  assert(as_string(123) == "123");
  assert(as_string(std::string("hello")) == "hello");
  assert(as_string("hello") == "hello");
}

畳み込み式

畳み込み式を使用して可変長テンプレート引数パック全体に対して一度に演算を適用するコードを 1 行で書くことができる。

// g++ -std=c++17 -Wall -O2 -o fold fold.cpp && ./fold
#include <iostream>
#include <cassert>

template<typename... T> auto reduceLeftSum(T... args) {
  return (... + args);    // Reduce Left: ((arg1 + arg2) + arg3) + ...
}
template<typename... T> auto reduceRightProd(T... args) {
  return (args * ...);    // Reduce Right: arg1 * (arg2 * (arg 3 * ...))
}
template<typename... T> void printAll(const T&... ts) {
  std::cout << "[";
  ((std::cout << ts << ' '), ...);   // std::cout << ts1 << ' '; std::cout << ts2 << ' '; ...
  std::cout << "]" << std::endl;
}
template<typename T, typename... TS> auto foldLeftSum(T init, TS... args) {
  return (init + ... + args);  // Fold Left: init + ((arg1 + arg2) + arg3) + ...
}

int main() {
  assert(reduceLeftSum(1, 2, 3, 4) == 10);
  assert(reduceRightProd(2.0, 3.0, 4.0) == 24.0);
  printAll(1, "Two", 3.0);    // [1 Two 3 ]
  assert(foldLeftSum(0) == 0);
  assert(foldLeftSum(0, 5, 6) == 11);
  return 0;
}

空パラメータパック (empty parameter pack) に対して使用する場合は初期値付きの二項畳み込みを使用することができる。

単項左畳み込み (... op args) ((arg1 op arg2) op arg3) ...
単項右畳み込み (args op ...) arg1 op (arg2 op (arg3 op ...
二項左畳み込み (value op ... op args) (((value op arg1) op arg2) op arg3) ...
二項右畳み込み (args op ... op value) arg1 op (arg2 op (arg3 op ... (argN op value)))
演算子 ., ->, [] を除くすべての二項演算子

畳み込み式のオペランドに式や関数を呼び出しを指定した場合、パラメータパックの各要素に対してその式や関数呼び出しが順次実行される。

// (((std::cout << (1 + f(arg1))) << (1 + f(arg2))) << ...) << std::endl;
(std::cout << ... << (1 + f(args)) << std::endl;

畳み込み式はテンプレートにおける可変数の型パラメータにも有効である。

template<typename... BS> class X: private BS... {
  public:
    void print() { (... , BS::print()); }   // すべての基底クラスの print() を呼び出す
}
struct A { void print(){ std::cout << "A::print()" << std::endl; } }
struct B { void print(){ std::cout << "B::print()" << std::endl; } }
struct C { void print(){ std::cout << "C::print()" << std::endl; } }

int main(){
  X<A, B, C> x;
  x.print();
}

他の言語では Python の sum(args) や JavaScript の array.reduce((a,b)=>a+b) に類似する機能だが、C++ の畳み込み式はコンパイル時に完全に展開される点で異なる。

新属性と構文改善

属性の追加
[[nodiscard]] 関数やメソッドの返値が使われていないときに警告を発する。
[[maybe_unused]] 名前やエンティティがスコープ内で使われていないときの警告を抑制する。
[fallthrough]] switch 文で意図して break せず、後の処理を実行するときの警告を抑止する。
属性の拡張

属性値は namespaceenum の各フィールドにも付けられるように拡張された。

ネストされた名前空間

二項演算子 a.b, a->b, a->*b, a[b], a << b, a >> b は左オペランド評価後に右オペランド、各代入演算子 b @= a は右オペランド評価後に左オペランドの順序で評価されることが保証されるようになった。また new 式においては (1) メモリ割り当て、(2) コンストラクタ引数評価、(3) オブジェクト初期化、の順序が保証されるようになった。

ただし f(a, b, c) のような関数呼び出しの引数の評価順序は依然として未定義。

標準ライブラリの強化

std::optional<T>

値の有無を表現するコンテナ型で、他の言語での Optional<T> (Rust, Scala) や Optional<T> (Java)、Haskel の Mayby モナドに相当する。

// g++ -std=c++17 -Wall -O2 -o optional optional.cpp && ./optional
#include <optional>
#include <iostream>

int main() {
  std::optional<int> value = 4812;
  std::cout << *value << std::endl; // 4812

  std::optional<int> zero = 0;
  if(zero) {
    std::cout << *zero << std::endl;  // 0
  }

  std::optional<int> none = std::nullopt;
  std::cout << std::boolalpha << static_cast<bool>(none) << std::endl;  // false
}

オプション

文字列ビューとバイト列

ファイルシステムライブラリ

並列アルゴリズム

追加ユーティリティ

メモリと並列処理

参考文献

  1. Nicolai M. Josuttis. C++17 - The Complete Guide (2019)