C++1z if constexpr文

C++1zから、コンパイル時条件によって分岐するif constexpr文が導入されます。これにより、再帰やヘルパ関数を書かなくて済むケースが多くなります。

D言語にあるstatic if文のようなものです。

template <class T, class... Rest>
void g(T&& p, Rest&&... rs)
{
    if constexpr (sizeof...(rs) > 0) {
        g(rs...); // rs...が空のときのオーバーロードが不要
    }
}

elseの方にはconstexprは必要ありません。 テンプレート内で条件分岐した場合は、到達しなかったブロックはインスタンス化されません。

ただし、条件式内でのコンパイル時条件については全てインスタンス化され、短絡評価によって後ろの方の条件がインスタンス化されないことを期待するようなコードは書けないので注意してください。

if constexpr (has_value_type_v<T> && typename T::value_type())

このような条件式は、else節に進む場合にコンパイルエラーになります。このような条件を書きたい場合は、条件分岐を入れ子にする必要があります。

if constexpr (has_value_type_v<T>) {
    if constexpr (typename T::value_type()) {
        …
    }
}

なお、constexpr ifでなくif constexprになっているのは、前者を採用すると構文として、else ifを書くときにconstexprが二重に必要になってしまうのを避けるためです。

// 採用された構文
if constexpr (cond) {
}
else if constexpr (cond) {
}
else {
}
// 却下された構文
constexpr if (cond) {
}
constexpr else constexpr if (cond) {
}
constexpr else {
}

最初に考えられていたstatic if宣言と違って、if constexpr文はスコープを導入します。また、型や関数を定義する条件分岐には使用できません。

// こういうことはできない。
// 条件によって関数や型の宣言・定義を変えるようなことはできないし、
// クラススコープでif constexpr文は使用できない
struct X {
    if constexpr (cond) {
        void f();
        using int32 = int;
    }
    else {
        void g();
    }
};

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++1z 非推奨だった古い例外仕様を削除

C++11でnoexceptが導入されたことによって非推奨になっていたthrowキーワードによる関数の例外仕様が、C++1zで削除されます。

throwキーワードによる例外仕様とは、void f() throw(std::runtime_error)のように、その関数がどの種類の例外を送出する可能性があるかを列挙する機能です。Javaの検査例外に近いものです。これはジェネリックプログラミングと相性が悪いことで問題視されていました。

C++1z以降、throwキーワードを使用した例外仕様のコードはコンパイルが通らなくなりますのでご注意ください。throw()noexceptと等価な効果を持つ機能として、非推奨のまま残ります。例外送出のthrowは変わりません。

// C++11から非推奨だがC++14まで合法。C++1zからコンパイルエラー
//void f() throw(std::runtime_error);

// C++11からは、noexceptを付けないことにより
// 「なんらかの例外が送出される可能性がある」ことを表現するか、
void f();

// コンパイル時の条件式によって、なんらかの例外が送出される可能性の有無を指定する
void f() noexcept(cond);

throwキーワードによる例外仕様の削除にともない、関連して非推奨だった以下の機能が削除されます:

これらの機能は、例外仕様で指定されていない例外が送出された場合に発生するエラーを検知するためのものですが、noexceptキーワードによる例外仕様では、例外は送出されるかされないかのみで、なんの例外が送出される可能性があるかは扱わないため、この種のエラーは発生しません。

参照

修正履歴

  • コメント欄での指摘を受け、throw()を使用したコードがC++1zからコンパイルエラーになる記述を削除。throw()が非推奨なまま残ることを記載

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++1z 例外仕様を型システムの一部にする

C++1zからは、noexceptによる関数の例外仕様が、型の一部として扱われるようになります。

互換性のためにnoexceptな関数ポインタから非noexceptな関数ポインタには変換できます。しかし、非noexceptな関数ポインタからnoexceptな関数ポインタには、変換できません。

void f() noexcept;
void g();

void(*p1)() noexcept = f;   // OK
void(*p2)() = f;            // OK : noexceptから非noexceptへの変換
//void(*p3)() noexcept = g; // コンパイルエラー : 非noexceptからnoexceptに変換できない

これは、ラムダ式も同様になります。

void(*p1)() noexcept = []() noexcept {}; // OK
void(*p2)() = []() noexcept {};          // OK
//void(*p3)() noexcept = [](){};         // コンパイルエラー

noexceptの違いによって関数をオーバーロードすることはできません。(戻り値の型と同様)

この仕様による破壊的な影響として、C++14まで有効だった以下のコードは、C++1zではコンパイルエラーになります:

void g1() noexcept;
void g2();

template<class T>
int f(T*, T*);

int x = f(g1, g2); // コンパイルエラー : g1とg2の型が一致しない (関数テンプレートの推論失敗)

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++11 最近接偶数への丸め

C++11では<cmath>にいろいろ関数が追加されていますが、丸め演算も増えています。

C++11で最近接偶数への丸めを使いたい場合は、std::nearbyint() (near by int)という関数がありますので、それを使いましょう。

#include <iostream>
#include <cmath>

int main()
{
    std::cout << std::nearbyint(2.5) << std::endl; // 2が出力される
}

四捨五入も最近接偶数への丸めも、どちらも大きなくくりでは「round to nearest integer (最も近い整数への丸め)」に該当します。この2つの丸め方式は、中間値(0.5)の場合に動作が異なります:

  • 四捨五入は中間値の場合に、ゼロから遠い方向に丸められる (2.5だったら3になる)
  • 最近接偶数への丸めは中間値の場合に、偶数方向に丸められる (2.5だったら2になる)

最近接偶数への丸めの必要性は、「「銀行家の丸め」とは何か - 会計SEのメモ」このあたりの記事がわかりやすいでしょう。

std::nearbyint()関数の実際の動作として、四捨五入になるか最近接偶数への丸めになるかは、ISO/IEC 60559 (IEEE 754)規格の推奨によって決まります。この規格ではround to nearestの丸め方式として最近接偶数への丸めを推奨しているため、この規格に準拠した環境では、std::nearbyint()関数は最近接偶数への丸めとして動作します。

補足として、

  • std::nearbyint()関数は、スレッドローカルな丸めモード状態に依存して丸め方式が変わるので注意
  • 似た動作の関数としてstd::rint()というものもあるが、丸めが発生したときに浮動小数点例外が発生するので使いにくそう。ISO/IEC 60559 (IEEE 754)規格で、std::nearbyint()関数とstd::rint()関数両方の動作が定義されているらしい

その他C++11での丸め方式

  • 昔からあるstd::ceil()std::floor()は切り上げ、切り下げと呼ばれるもので、より正確な動作の説明としては「正の無限大方向への丸め」「負の無限大方向への丸め」と言う
  • C++11から追加されたstd::trunc()は切り捨てだが、std::floor()と違って「ゼロ方向への丸め」というものになっている。この2つは、負の値に対する動作が違うので注意
  • C++11から追加されたstd::round()は四捨五入と決まっている。最近接偶数への丸めではないので注意

C++1z 畳み込み式

C++1zでは、「畳み込み式 (Fold expressions)」という機能が入ります。これは可変引数テンプレートのパラメータパックに対して集計操作を行うためのものです。

たとえば、整数のシーケンスを与えてその合計値を求める計算は、以下のように書けます:

#include <iostream>

template <class... Args>
int sum(Args... args)
{
    // 「... @ args」が畳み込み式。@の部分は任意の二項演算子
    return (... + args);
}

int main()
{
    int x = sum(1, 2, 3, 4, 5);
    std::cout << x << std::endl; // 15
}

このような計算はこれまで再帰を使用して実装する必要がありましたが、より簡潔に書けるようになりました。

ここでは、argsパラメータパックに含まれる全要素を、+二項演算子を使って集計しています。...演算子の左辺に置いているので、左から畳み込みが行われます。計算過程としては、以下のようになります:

1 + 2
3 + 3
6 + 4
10 + 5

...演算子の右辺に置くと、右から畳み込まれます:

template <class... Args>
int sum(Args... args)
{
    return (args + ...);
}

計算過程:

4 + 5
3 + 9
2 + 12
1 + 14

accumulate()アルゴリズムと同様に、初期値をくっつけることもできます:

template <class... Args>
int sum(Args... args)
{
    return (0 + ... + args);
}

畳み込み式で使用できる演算子

畳み込み式で使用できるのは、以下の二項演算子です:

+  -  *  /  %  ^  &  |  ~  =  <  >  <<  >>
+=  -=  *=  /=  %=  ^=  &=  |=  <<=  >>=
==  !=  <=  >=  &&  ||  ,  .*  ->*

前述した例の+演算子以外ですと、たとえば条件式のシーケンスに対して「全てtrueか」「いずれかがtrueか」「全てfalseか」を調べる関数は、以下のように書けます:

#include <cassert>

// 全てのパラメータがtrueならtrueを返す
template <class... Args>
bool all(Args... args)
{
    return (true && ... && args);
}

// いずれかのパラメータがtrueならtrueを返す
template <class... Args>
bool any(Args... args)
{
    return (... || args);
}

// 全てのパラメータがfalseならtrueを返す
template <class... Args>
bool none(Args... args)
{
    return !any(args...);
}

int main()
{
    bool a = all(true, true, true);
    bool b = any(false, true, false);
    bool c = none(false, false, false);

    assert(a == true);
    assert(b == true);
    assert(c == true);
}

パラメータパックが空の場合のデフォルト値

パラメータパックがからの場合のデフォルト値は、演算子によって変わります:

演算子
* 1
+ int()
& -1
| int()
&& true
|| false
, void()

この表に記載されていない演算子を空のパラメータパックに対して適用すると、コンパイルエラーになります。

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++1z トライグラフを削除

C言語から引き継がれた「トライグラフ (trigraph)」という機能は、ASCIIより小さなISO/IEC 646という文字コードでもプログラムが書けるように用意された機能で、いくつかの文字の代替表現を使用できるようにするものです。

トライグラフ表現 置き換え後の文字
??= #
??/ \
??' ^
??( [
??) ]
??! |
??< {
??> }
??- ~

以下のトライグラフを使用したコードは、

??=define arraycheck(a,b) a??(b??) ??!??! b??(a??)

以下のように置き換えられます:

#define arraycheck(a,b) a[b] || b[a]

文字コードの問題がなくなった現代では、トライグラフは不要になりました。ユーザーにとってわかりにくいこともあり、多くのコンパイラがトライグラフをデフォルトで無効にしたり警告を出力したりといった対応をしていました。

C++1zの策定作業にともない、トライグラフの利用状況が大規模に調査されました。この機能が使用されていることはほとんどないということがわかり、C++1zからはトライグラフが削除されることが決定しました。

今後、トライグラフを使用したC++コードはコンパイルが通らなくなりますので、注意してください。C言語の方はC11時点でまだ残っています。

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++1z 構造化束縛

C++1zから、タプルやクラスを分解する「構造化束縛 (Structured Binding)」という機能が入ります。他の言語では多重代入のように呼ばれているもので、C++11標準ライブラリのstd::tie()関数の代わりとして使用できます。

以下は、タプルを返す関数の戻り値を、分解して受け取るコードです:

tuple<int, string> f();

// aにはintの値、bにはstringの値が代入される
auto [a, b] = f();

構造化束縛には、autoプレースホルダーを型として指定したあと、角カッコ [ ] 内に、分解した値を受け取る変数名のリストを記述します。各変数に対して個別に具体的な型を指定することはできません。

タプルの場合は、std::tuple_size<T>::valueという式が妥当な型が対象となり、タプルのインタフェースget<I>(t)std::tuple_element<I,T>::typeが使用できる型であれば、std::tupleと同様に構造化束縛で分解できます(ADLで呼び出すため、getにはstd::が付いていない)。標準ライブラリでは、std::pairstd::arrayもタプルとして分解できます。

これを使用して、map::insert()の戻り値を簡単に受け取れたりしますね。

auto [iter, success] = m.insert(x);
// iterは、挿入された要素を指すイテレータ
// successは、挿入に成功したかを表すbool値 (重複時に失敗する)

クラスの分解

構造化束縛はタプルだけでなく、クラスも分解できます。クラスは、非静的publicメンバ変数が列挙されます(public基本クラスのものを含む)。そのクラスは、無名共用体メンバを持ってはいけません。

struct Point { int x, y; };
Point f();

// xにはPoint::xの値、yにはPoint::yの値が代入される
const auto [x, y] = f();

メンバ変数は、ビットフィールドになっていても分解できます。

struct X {
    int flag : 1;
    int counter : 15;
};

X f();
const auto [flag, counter] = f();

組み込み配列の分解

組み込み配列を分解することもできます。その際、受け取る変数名のリストは、配列の要素数と同じ個数だけ指定する必要があります。要素数が一致しない場合はコンパイルエラーになります:

// 配列の各要素を取得。
// 受け取る個数が配列の要素数と一致していなければならない
int ar[] = {3, 1, 4};
auto [a, b, c] = ar;

参照修飾

変数を参照として受け取りたい場合は、autoのあとに参照の修飾をします。以下は、範囲for文でstd::mapオブジェクトの要素をキーと値に分解するコードです:

std::map<int, string> m;

for (const auto& [key, value] : m) {}

属性の指定

属性は、分解した個別の変数に対しては指定できず、全体に対して指定することになります:

[[maybe_unused]] auto [a, b] = f();

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

新装版 達人プログラマー

書くのが遅くなってしまい、発売からけっこう経ってしまいましたが、オーム社さんから『新装版 達人プログラマー』をいただきました。ありがとうございます。

f:id:faith_and_brave:20161117174934j:plain:w480

本書は、ピアソン・エデュケーションから出版されていた『達人プログラマー』を、オーム社が内容を新たに再出版したものとなっています。

改定内容は、オーム社のブログで紹介されています。

本書は、原書が出たのが1999年、日本語版が出たのが2000年となっており、かなりの時間が経過しています。今回の改定で、本書内で紹介されているツールに最近のものが追加されていたりとうれしい変更が多々入っていますが、その変更がたとえ入っていなかったとしても、ソフトウェア開発の手法、プログラマとしての取り組み方として大事なものが2016年のいまでも変わらないものが多くあることを再認識できました。

このようなソフトウェア開発とプログラマとしての取り組み方を包括的に解説した書籍として、本書『達人プログラマー』の代わりになるものは私が把握している限りではありません。書籍によって目標とするものが違うため、『達人プログラマー』とは異なるアプローチで学べる本はいくつかありますが(この記事の最後の方にまとめます)、本書を読む価値はいまだ下がることはありません。

私が行っているプログラマへの教育の仕事でも、教科書として本書の旧版を使用していました。わりと最近のことです。それだけに、新装版が出版されて、本書がまた手に入りやすくなったのはとてもうれしいことだと思います。

プログラマ、開発者として成長したい方、またはその教育者の方には、2016年のいまでもぜひ読んでいただきたいです。

他の書籍

『達人プログラマー』とは異なるアプローチですが、プログラマとしての取り組み方として参考になる書籍をいくつか挙げます。

これらの書籍は、ソフトウェア開発全般や技法を学ぶというよりは、プログラマとしての取り組み方、成長の仕方を主眼にしています。私はプログラマに最も大事なものは「生涯学習を身につけること」だと考えているのですが、これらの書籍ではそういったことがよく学べるでしょう。

C++1z 単一要素の波カッコ初期化をTに推論する

単一要素の波カッコ初期化(braced init list)を、(代入構文ではなく)直接autoで受けた場合のルールが、以下のように変更になります:

auto a {1};      // C++14まではinitializer_list<int>
                 // C++1zではint

auto b {1, 2};   // C++14まではinitializer_list<int>
                 // C++1zではコンパイルエラー

auto c = {1};    // これまで通りinitializer_list<int>
auto d = {1, 2}; // これもinitializer_list<int>

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。

C++1z if文とswitch文の条件式と初期化を分離

for文が for (初期化式; 条件式; 加算式)になっているのと同様に、if文とswitch文にも、条件式の前準備としての初期化式をif (初期化式; 条件式)のように書けるようになります。

// mapへの要素挿入
if (auto p = m.try_emplace(key, value); !p.second) {
  // 要素挿入に失敗 (すでにkeyが登録されている)
}
else {
  // 挿入した要素を使用して何か処理する
  process(p.first);
}
if (status_code c = open_file(); c != SUCCESS) {
  // 処理に失敗
  // 失敗の原因ごとに処理をする
  if (c == NO_FILE) {
  }
  if (c == FORMAT_ERROR) {
  }
}
switch (Button button(args); Status s = button.status()) {
  case OK: break;
  default: throw BadStatus(toString(s));
}

構文としては、

if (condition)
if (init-statement; condition)

のように2種類用意されるのではなく、初期化式を省略可能にして、以下のように定義されます。

if (init-statement;(opt) condition)

そのため、後に紹介するif constexpr文でも初期化式は使用できます。

参照

お断り

この記事の内容は、C++1zが正式リリースされる際には変更される可能性があります。正式リリース後には、C++日本語リファレンスサイトcpprefjpの以下の階層の下に解説ページを用意する予定です。