2015-03-11

最近のC++17事情

C++1z、あるいはC++17とも呼ばれている次のC++規格の、最近の事情はどうなっているのか。すでにドラフトに取り入れられた機能もあるので、現在の最新の状況を見ていこう。もうすでに紹介したものも含まれているが、おさらいとしてみていく。また、ここで解説する新機能は、いずれもすでにドラフト入りしているが、正式に規格制定される際に変わる可能性がある。

N3928: メッセージなしstatic_assert

C++11で入ったstatic_assertは、必ず文字列リテラルを書かなければならなかった。

static_assert( INT_MAX >= 2147483647, "This code assumes at least 32-bit int." ) ;
static_assert( true == true, "You're compiler is fundamentally wrong." ) ;

しかし、この文字列リテラルの使われ方は規定されていない。特にメッセージを書きたくなくても、文字列リテラルは、文法上必ず書かなくてはならない。


static_cast( std::numeric_limits<double>::is_iec559, "" ) ;

C++1zでは、文字列リテラルを書かなくてもよくなる。


static_cast( false ) ;

N4086: Removing trigraphs??!

トライグラフが取り除かれる。

// C++14までは#
// C++1zでは??=
std::cout << "??=" ;

N4051: Allow typename in a template template parameter

テンプレートテンプレートパラメーターにtypenameキーワードが使えるようになる。

template <
    template < typename T >
    class U
>
struct X ;

が、

template <
    template < typename T >
    typename U
>
struct X ;

とも書けるようになる。

N3922: New Rules for auto deduction from braced-init-list.

auto指定子で直接初期化でリスト初期化を書いた場合の挙動を変更する。C++14で合法なコードが違法になる珍しいケースだ。


auto a{ 1, 2, 3 } ;

このコードは、C++14までは合法なコードであり、decltype(a)は、std::initializer_list<int>となる。

C++1zでは、このコードは違法である。auto指定子で直接初期化でリスト初期化の場合は、単一のinitializer-clauseしか書くことができなくなった。

// aの型はint
auto a{ 1 } ;

N4295: Fold expressions

今回紹介する新機能の中で、一番面白い機能がこれだ。パラメーターパックに対するfold式がC++に入る。

パラメーターパック全てに対して演算子を適用したい場面がある。

// 与えた実引数にoperator +を適用して合計値を返す関数テンプレートsum
sum( 1, 2, 3 ) ; // 6
sum( 1, 2, 3, 4 ) ; // 10

このようなsumをC++14で書くと以下のようになる。

template < typename T >
T sum( T && t )
{
    return t ;
}

template < typename T, typename ... Args >
T sum( T && t, Args && ... args )
{
    return t + sum( std::forward<Args>(args)... ) ;
}

やりたいことは、パラメーターパックのそれぞれの実引数にoperator +を適用したいだけなのに、やたらと面倒なコードだ。

そこで、C++1zには、パラメーターパックに対するfold式が入る。

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

これは、sum( 1, 2, 3, )に対して、 (((1 + 2) + 3) + 4)のようにleft foldされる。

逆に以下のように書けば、

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

(1 + ( 2 + ( 3 + 4 ) ) )のように、right foldされる。

fold式は、必ず括弧で囲まなければならない。


( pack + ... ) ; // OK
pack + ... ; // エラー、括弧で囲まれていない

fold式には、unary(単項) foldとbinary(二項) foldがある。binary foldは、(e1 op1 ... op2 e2)という形を取る。op1とop2は同じfold演算子でなければならず、e1とe2は、どちらか片方だけがパラメーターパックでなければならない。e2がパラメーターパックであった場合はleft fold、e1がパラメーターパックであった場合はright foldとなる。


template < typename ... Types >
void f( Types ... args )
{
    ( 1 + ... + args ) ; // binary left fold
    ( args + ... + 1 ) ; // binary right fold
}

それぞれ、(( ( 1 + args0 ) + args1) + ... + argsN )と、args0 + ( args1 + ( ... argsN + 1) )のようにパック展開される。

N4267: Adding u8 character literals

UTF-8文字リテラルの追加。プレフィクスu8の文字リテラルで、UTF-8のコード単位一つで表現可能な文字が記述できる。


char a = u8'a' ; // OK
char b = u8'あ' ; // エラー、UTF-8で符号化するにはコード単位が3個必要。

N4230: Nested namespace definition (revision 2)

名前空間の宣言をネストできるようになる。

namepsace A {
    namespace B {
        namespace C {
            // ...
        }
    }
}

のようなコードが、

namespace A::B::C {
// ...
}

のように書ける。

N4266: Attributes for namespaces and enumerators

名前空間とenumeratorにattributeが記述できるようになる。C++14までは、文法上の制約で記述することができなかった。これにより、名前空間やenumeratorにdeprecatedが指定できる。記述する場所は、名前空間ならnamespaceキーワードの前、識別子の後、enumeratorならば、識別子の後だ。


namespace [[deprecated("This namespace is deprecated.")]] libv1 { }

enum class E { value [[deprecated("This enumerator is deprecated.")]] = 0 ; }

N4268: Allow constant evaluation for all non-type template arguments

すべての非型テンプレート実引数でコンパイル時評価を可能にするように制限を緩和する。

以下のコードは違法である。

template<int *p> struct A {};
int n;
A<&n> a; // ok

constexpr int *p() { return &n; }
A<p()> b; // エラー

理由は、定数として渡せるポインターはnullだけだからである。

constexpr int *p() { return nullptr ; }
A<p()> b; // OK

constexprがある今、この制約は時代にそぐわない。そこで、非型テンプレート実引数に、任意の定数式を渡せるように制限を緩和された。つまり、上のコードは違法ではなくなる。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

19 comments:

Anonymous said...

s/static_cast/static_assert/

Anonymous said...

私は一度も職業プログラマをしたことのない門外漢だ。20年前に趣味でストラウストラップの分厚い本とBorland C++でC++を勉強していたことがあるだけだ。
その当時のC++でさえ、言語仕様はとても大きい物だったと認識されていたはずだ。にも関わらず、ずっと続いているようにみえる新しい言語要素の追加はC++という言語をあまりに大きなものにしていないだろうか。
私のような門外漢にはきっと主要な言語仕様を覚えることもおぼつかない、と想像している。
自分がゼロからコードを書くのならともかく、他人が書いたC++ソースコードを触るのならば言語仕様の全体をおよそ把握していなければならない。だから新しい職業プログラマがC++に挑戦する時には、実戦に投入される前に相当の訓練を受ける必要があるのではないだろうか。
こうしたあまりに大きな言語仕様は、新たな初心者プログラマがC++という言語を身につけるにはあまりに大きい障壁になり、C++プログラマの増加を妨げるのではないかと危惧している。これは長期的に見てC++という言語の衰退や消滅をもたらさないだろうか。

もちろん他の言語ではなくC++でなければならない決定的な差が存在しているのであれば別だけれども。

Anonymous said...

祝トライグラフ削除。
ウニコードは32ビットcharで動けばいいですよ。
こんなマジック使わなくてもいい時代になったんですからね。
ただチープにリーズナブルにという思想は失われるべきではないですね。
バランスこそ真理ですよ。

Anonymous said...

fond expressionの存在理由って何なんでしょう?
再帰を書くのが面倒っても、標準アルゴリズムに一個足せば終わりだと思うんですが。
私は何を見落としているんでしょう。

Anonymous said...

fond expressionの存在理由は、コンパイラは、(無限の再帰に関する)停止
性問題に対する回避策として、再帰の深さに対して制限をもっているので、
現在は、再帰深度を抑えるための工夫として、
http://fimbul.hateblo.jp/entry/2014/06/29/024918
のようなことをしないと駄目ってのが背景があるわけですが、
パラメータパックの展開自体は高々有限個なので、必ず終わることが保障できます。
ということで、アルゴリズムの追加だけではなくコンパイラに対して(この
再帰はすごい深いかもしれないけど絶対終わるから という)何らかの文法上
の手当てをする必要があるというところではないかなと

ついでに、末尾呼び出しの最適化が出来るのでループに展開できてスタックオー
バーフローの懸念もなくなって再帰のための大量のシンボルも消せて、万歳み
たいな感じじゃないですかね。

というように思いましたがどうなんでしょ。

Anonymous said...

読みやすいというだけで大きなメリット、存在理由になると思います。

Anonymous said...

読みやすさって、たとえば+演算子なら、誰かがsum()を書いたらそれで終わりじゃないですか。
みんなsum()を使うから、fold式なんて誰も使わない。目にすらしないのに、読みやすさに何の意味があるんですか。

Anonymous said...

これが標準ライブラリにあればfold式いらないんじゃないかと思うんです。

template <typename F, typename T>
auto rfold(const F&, T&& a) {
 return a;
}

template <typename F, typename T, typename... U>
auto rfold(const F& f, T&& a, U&&... b) {
 return f(std::forward<T>(a), rfold(f, std::forward<U>(b)...));
}

ΣだのΠごときどうせ一回しか書かれないんですから。

template <typename... T>
auto sum(T&&... a) {
 // return (a+...+0); だと暗黙的に変換される可能性があるから、同じ結果になるとは限らないが
 return rfold(std::plus<>(), std::forward<T>(a)...);
}

template <typename... T>
auto prod(T&&... a) {
 // return (a*...*1); だと以下同文
 return rfold(std::multiplies<>(), std::forward<T>(a)...);
}

汎用性もありますし。

template <typename... T>
auto dot(T&&... a) {
 return lfold([](const auto& a, const auto& b){ return dot(a, b); }, std::forward<T>(a)...);
}

fold式の何がおもしろいのか本当にわからないんです。

Anonymous said...

range based forも同じ事を古いforでも出来たんだから存在意義が無いとか面白くないとか言っちゃう人ですか?

Anonymous said...

あれ、誰かRange-based forの話なんかしてるんですか?

私は、fold式が、使い道がほとんど無く、使ったとしても既存のTMPに劣る、何の意味も無い言語拡張だと思う、というだけです。
江添さんもずいぶん持ち上げてるし、何か私の知らない画期的なメリットがあるのかもしれません。
誰か教えてくれませんか。

Anonymous said...

再帰のトリックを用いないと使い物にならないような欠陥言語仕様の穴を塞ぐために
ようやくマトモな使い方ができるよう言語仕様を見直したんですよ。
機能的に不可能か可能かの問題ではないです。
それに再帰の深さの問題ついてはスルーですか

Anonymous said...

再帰はトリックでもなんでもない普通の手法です。欠陥と思うならディレクトリすら使えないですねw
深さったって高々関数引数の個数でしょ?
かりに欠陥だとして、
- 演算子ごとに一度しか使えない
- 演算子以外に使うには演算子オーバーロードか、結局TMPで再帰を書くことになる
- 暗黙的な変換を防ぐためには結局オーバーロードが必要
これで穴がどうふさがったんですかw?
結論ありきでこじつけた理由じゃなくて、本当のメリットが知りたいんですが。

Anonymous said...

> 今回紹介する新機能の中で、一番面白い機能がこれだ。
ネタ的に面白いって意味だったりして。
比較演算子のfoldなんて真面目に考えてるわけが無い。

Anonymous said...

結局は、operatorが全ての元凶なのではなかろうか?
operatorさえ消滅すれば、
非staticメンバーoperator関数とstaticメンバーoperator関数の混乱など存在しえず、
カンマ演算子と引数区切りのカンマの多義性も、関数呼び出しの()と優先順位の()の多義性も、配列宣言の[]と要素アクセスの[]の多義性も解消され、
式パーサーは単純化され、
予約語が一つ少なくなり、
もちろんfold式なんてものも必要なくなる。
こんなに素晴らしい事はないではないか。

Anonymous said...

C++の言語仕様を変える必要ないじゃないですか。
LISPを使っちゃいかがですか。

Bolero MURAKAMI said...

わたしは中3女子だが、「再帰のトリック」とは「再帰がトリックである」の意ではなく、「再帰を抑えるためのトリック(イディオム)が必要となる」の意ではないだろうか。
例えばTMPでの count や all_of の実装において、単純な実装ではO(N)、工夫して少なくともO(logN)の再帰深度を必要とするのに対し、Fold expressionでは再帰を記述せずに実装できる。
これは記述が楽になるのみならず、コンパイル時間の短縮も期待でき、明確なメリットであると言える。

Anonymous said...

static_cast( false )
とは

Anonymous said...

Bolero MURAKAMI様
工夫したら、型によっては結果が変わっちゃうじゃないですか。計算順が変わるんだから。
それに、再帰ったってたかだか関数引数の個数でしょ?
コンパイル時間が問題になるほど関数引数並べるのって、そんなにメジャーケースですか?
コンパイラの仕様を変えるほどに? コンパイル時にレイトレーシングするのが、世界中のC++ユーザの負担を増やすほど大事ですか?

Anonymous said...

日本語圏の掲示板ってやっぱりすぐこうやって険悪な雰囲気で話したがるな。
だからstackoverflowとかのStack Exchange系のサイトが好きで、teratailとかそういうのは使わないんだよな。
stackoverflowの日本語版も結局同じ雰囲気になっちゃったな。