#C++のつまずきポイントを無駄に詳説
はじめまして(?)、いなむ先生と申します
C++アドベントカレンダー12日目は
オンラインコミュニティ「C++の会」での質問や
読書会の会話や
自分の体験をもとに
C++のつまずきポイントを解説します(会話の焼きましです
Template大好きな人なのでTemplate関連が多い(すべて?
##std::arrayのuniform initialization
C++11からはuniform initializationがサポートされていますが
std::array
はコンストラクタが定義されておらず
組み込み配列と同様に初期化リストで初期化する値を指定できます
ではさっそく、
std::array< int, 4 > a{ 1, 2, 3, 4 } ; // error!
残念ながらエラーです
std::arrayはメンバ変数として配列を持つため初期化子が配列を初期化する必要があります
配列の初期化はリスト初期化です
つまり、二重に波カッコを書く必要があります
もしくは初期化代入にするかのどちらかです
std::array< int, 4 > a{ { 1, 2, 3, 4 } } ; // OK!
std::array< int, 4 > b = { 1, 2, 3, 4 } ; // OK!
OKなのですが下のコードはclangだとwarningになり波カッコを二重にするよう促されるようです
##autoの注意点
auto
は非常に便利ですが、幾つか知っておくべきことがあります
###autoの一意性
一つのauto
は一つの型を表していなければなりません
例えば、次のようなコード
auto a=1,b=1.0; // error! aとbは同じ型に推論されなければならない
auto
は一意でなければならないのでa
がint
, b
がdouble
に推測されるこのコードはill-formedになります
###autoとtemplateの違い
auto
は{}
で初期化した場合には必ずstd::initializer_list
であると推測します
対してテンプレートパラメータT
はstd::initializer_list
とは推測しません
template < typename T>
void f( T ) {} ;
auto x = { 1, 3, 5 } ; // x's type is std::initializer_list<int>
f( { 1, 3, 5 } ) ; // can't deduce T
この関数の推測は、引数を未知の型T
のinitializer_list
とすることで解決できる
template < typename T>
void f( std::initializer_list<T> ) {} ;
f( { 1, 3, 5 } ) ; // OK! T is int, and parameter type is initializer_list<int>
C++14からは、関数の戻り値をauto
にすることもできる
ここで注意すべきなのは戻り値をauto
にした関数の戻り値の推論は
autoの規則ではなく、__templateの規則で行われる__ということだ
つまり、戻り値に記述されたbraced-init list( {}で囲まれたリスト )
がinitializer_list
と推論されない
auto f()
{
return { 1, 3, 5 } ; // can't deduce !
}
##コンテナの選び方
「コンテナの種類が多くて、どれを使えばいいのかわからない」
なんてことはあるかもしれません
たくさんの種類がありますが
最有力候補はvector
です
単純なゆえにほとんどの場合に高速です
固定長の配列を用いたいいたい場合は配列型(int[5]
のような)を用いるよりも高機能なarray
を用いるのがよいです
値の高速な検索を必要とする場合はmap
やunordered_map
のような連想コンテナを使用するとよいでしょう
mapと名のついたコンテナは固有のメンバ関数find()
をもち、内部構造を利用した高速な検索を提供します
以下に、標準ライブラリで提供されるコンテナの種類と特徴をまとめました
コンテナの名前 | コンテナの説明 | カテゴリ |
---|---|---|
vector | 動的配列 | シーケンスコンテナ |
array | 固定長配列 | シーケンスコンテナ |
list | 双方向リスト. 任意位置要素の挿入削除が高速 | シーケンスコンテナ |
forward_list | 単方向リスト | シーケンスコンテナ |
deque | 両端キュー | シーケンスコンテナ |
queue | キュー | コンテナアダプタ |
priority_queue | 優先順序付きキュー | コンテナアダプタ |
stack | スタック | コンテナアダプタ |
map | 連想配列. キーと値のペアを保持する. キーから値を取り出すのが O(log N) と高速 | 連想コンテナ |
multimap | 重複可能な連想配列 | 連想コンテナ |
unordered_map | ハッシュによる連想配列 | 非順序連想コンテナ |
unordered_multimap | ハッシュによる重複可能な連想配列 | 非順序連想コンテナ |
set | 順序付集合 | 連想コンテナ |
multiset | 重複可能な集合(多重集合) | 連想コンテナ |
unordered_set | ハッシュによる集合 | 非順序連想コンテナ |
unordered_multiset | ハッシュによる重複可能な集合(多重集合) | 非順序連想コンテナ |
- シーケンスコンテナ
- 要素の順序が維持されるコンテナ
- 連想コンテナ
- 要素が整列されて格納されるコンテナ
- 非順序連想コンテナ
- 要素に順序がなく、ハッシュを用いて管理されるコンテナ
- コンテナアダプタ
- 実際にはコンテナではなく、他のコンテナへの操作を一部制限する形でデータ構造を表現するコンテナ
##コンテナとイテレータ
イテレータとはコンテナとアルゴリズムを仲介するインターフェイスです
-
operator*
で値を取り出せる -
operator++
で次の要素をさすイテレータに進めることができる
を満たすものをイテレータとして扱うことができます
標準ライブラリのアルゴリズムは具体的なコンテナオブジェクトを操作するのではなく
イテレータにより横断可能な範囲を操作の対象とします
このことによりあらゆるコンテナに対してアルゴリズムが適用できるようになっています
コンテナには異なる種類のイテレータを取得できるメンバ関数(フリー関数もある)があるので目的に合わせて使い分けましょう
begin(), end()
: ふつうのiterator
要素を変更するときにどうぞ
std::vector<int> v{1,2,3,4,5} ;
for ( auto&& iter = v.begin(); iter != v.end(); ++iter)
*iter = 0;
cbegin(), cend()
: const_ieratorを取得
constイテレータ、要素を変更しないときにどうぞ
std::vector<int> v{1,2,3,4,5} ;
for ( auto&& iter = v.cbegin(); iter != v.cend(); ++iter)
std::cout << *iter << std::endl;
rbegin(), rend()
: reverse_iteratorを取得
逆順イテレータ、ケツから走査して要素を変更したいときにどうぞ
std::vector<int> v1{1,2,3,4,5,6,7,8,9};
std::vector<int> v2{1,2,3,4,5};
for(auto&& iter1 = v1.rbegin(), iter2 = v2.rbegin(); iter2 != v2.rend(); ++iter1,++iter2)
*iter2 = *iter1;
crbegin(), crend()
: const_reverse_iteratorを取得
const逆順イテレータ、ケツから走査して要素を変更しないときにどうぞ
std::vector<int> v{1,2,3,4,5} ;
for ( auto&& iter = v.crbegin(); iter != v.crend(); ++iter)
std::cout << *iter << std::endl;
イテレータには幾つか種類があり、できることが違います
###イテレータの分類
標準ライブラリで定義されるイテレータの分類
標準ライブラリが提供するアルゴリズムは次のいずれかのイテレータを引数として要求する
- 入力イテレータ(Input Iterator)
- イテレータの前進、要素の読み出しができる(std::istream_iterator)
- 出力イテレータ(Output Iterator)
- イテレータの前進、要素の書き込みができる(std::ostream_iterator, insert_iterator)
- 前方イテレータ(Forward Iterator)
- イテレータの前進、要素の読み出し、書き込みができる
前進イテレータは入力イテレータ、出力イテレータが要求される場合にも使用できる
(forward_list, unordered_map, unordered_setにおけるイテレータなどが該当) - 双方向イテレータ(Bidirectional Iterator)
- イテレータの前進と後退、要素の読み出し、書き込みができる
双方向イテレータは前進イテレータが要求される場合にも使用できる
(list, map, setにおけるイテレータなどが該当) - ランダムアクセスイテレータ(Random Access Iterator)
- イテレータの前進と後退、要素の書き込み、読み出し加えて、任意の位置にある要素にアクセスできる
ランダムアクセスイテレータは双方向イテレータが要求される場合でも使用できる
(array, deque, vectorにおけるイテレータなどが該当)
配列の要素をさすポインタもランダムアクセスイテレータとして扱うことができる
###iteratorとconst_iteratorとconst interator
以下のコードがすべてを語ってくれる
std::vector<int> v(4);
// normal iterator
std::vector<int>::iterator iter = v.begin();
// const_iterator
const std::vector<int>::iterator c_iter = v.begin();
// const interator
std::vector<int>::const_iterator const_iter = v.begin();
++iter; // OK
++const_iter; // OK
++c_iter; // c_iterはconstなので変更できない
*iter=3; // OK
*c_iter=3; // c_iterはconstだが, *c_iterはconstではないのでOK
*const_iter=3; // これはエラー
はっきり言って、インクリメントできないconst iterator
の出番はほぼない
また、当たり前だがconstな要素を持つコンテナのイテレータはconst_iterator
となる点には理解が必要
const std::vector<int> v(4);
auto&& const_iter = v.begin();
++const_iter ;
*const_iter=3; // これはエラー
##ラムダ式の推測
関数の中でラムダ式の戻り値や引数の型を推測したいなんてことがあるかもしれません
型推論はdecltype
を用いて
decltype( x + y )
のように書くとことができますが
ラムダ式をdecltype
の中に記述することはできません
また、ラムダ式はそれぞれユニークな型を持ちます
そこで、関数ポインタに変換すると型推論できます
#include <vector>
#include <iostream>
#include <type_traits>
#include <boost/type_index.hpp>
template<typename R, typename F, typename A>
auto return_helper(R(F::*)(A) const) -> R;
template<typename R, typename F, typename A>
auto arg_helper(R(F::*)(A) const) -> A;
template < typename F >
void func(F){
using Return_t = decltype(return_helper(&F::operator()));
using Arg_t = decltype(arg_helper(&F::operator()));
std::cout << "return type : " << boost::typeindex::type_id_with_cvr<Return_t>().pretty_name() << std::endl;
std::cout << "arg type : " << boost::typeindex::type_id_with_cvr<Arg_t>().pretty_name() << std::endl;
}
int main()
{
func([](double&&){return std::vector<int>{1,2,3};});
return 0;
}
実行結果
return type : std::vector<int, std::allocator<int> >
arg type : double&&
一応推論できています
##テンプレート型推論規則
テンプレートはC++に欠かせない機能です
テンプレートの型推論規則を知らないとよくつまずきます
テンプレート型推論規則ではT&&
やauto&&
はユニヴァーサル参照と呼ばれ特別扱いされます
「Effective Modern C++」の第1章にそって
関数のパラメータの宣言に分けて解説します
###テンプレート型推論その1(パラメータタイプが参照もしくはポインタ型)
template < typename T >
void func( T& ) ;
int x = 2 ; // x は int
const int cx= x ; // cx は const int
const int& rx = x ; // rx は x への const int 参照
func(x) ; // T は int, パラメータタイプは int&
func(cx) ; // T は const int, パラメータタイプは const int&
func(rx) ; // T は const int, パラメータタイプは const int&
T&
を仮引数にとる関数にconst修飾された型を渡すとconstは型の一部として扱われます
参照は読み飛ばされます
###テンプレート型推論その2(パラメータタイプがユニヴァーサル参照)
ユニヴァーサル参照を仮引数にとる場合は
左辺値が渡されたときにはT
は左辺値参照であると推論されます
右辺値が渡されたときはその1の規則が適用されます
template < typename T >
void func( T&& ) ;
int x = 2 ; // x は int
const int cx= x ; // cx は const int
const int& rx = x ; // rx は x への const int 参照
func(x) ; // xは左辺値, よって T は int&, パラメータタイプは int&
func(cx) ; // cxは左辺値, よって T は const int&, パラメータタイプは const int&
func(rx) ; // rxは左辺値, よって T は const int&, パラメータタイプは const int&
func(2) ; // 2は右辺値, よって T は int, パラメータタイプは int&&
特別な規則で右辺値と左辺値が区別されることを知っておかなくてはなりません
###テンプレート型推論その3(パラメータタイプが参照でもポインタでもない)
仮引数が参照でもポインタでもない場合は値渡しになり、その場合の規則は
- cv修飾は無視される
- 参照は無視される
となります
template < typename T >
void func( T ) ;
int x = 2 ; // x は int
const int cx= x ; // cx は const int
const int& rx = x ; // rx は x への const int 参照
func(x) ; // T は int, パラメータタイプは int
func(cx) ; // T は int, パラメータタイプは int
func(rx) ; // T は int, パラメータタイプは int
###テンプレート型推論:蛇足(仮引数がcv修飾されている場合)
N4140の14.8.2.1 Deducing template arguments from a function callを参照すると
3 If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a
reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cvunqualified
template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in
place of A for type deduction. [ Example:
template int f(T&&);
template int g(const T&&);
int i;
int n1 = f(i); // calls f(int&)
int n2 = f(0); // calls f(int&&)
int n3 = g(i); // error: would call g(const int&&), which
// would bind an rvalue reference to an lvalue
>—end example ]
要約すると、仮引数がcv修飾されている場合には推論の段階で引数の型のcv修飾は無視されます
その後、参照も無視されて型推論されます
```cpp
template < typename T >
void f( T& ) ;
template < typename T>
void g( const T& ) ;
int x = 2 ; // x は int
const int cx= x ; // cx は const int
const int& rx = x ; // rx は x への const int 参照
f(x) ; // T は int, パラメータタイプは int&
f(cx) ; // T は const int, パラメータタイプは const int&
f(rx) ; // T は const int, パラメータタイプは const int&
g(x) ; // T は int, パラメータタイプは const int&
g(cx) ; // T は int, パラメータタイプは const int&
g(rx) ; // T は int, パラメータタイプは const int&
となります
T&
を使うとconst性が伝播します
const T&
を使うとconst性を強制できます
また、const な lvalue reference はrvalueを束縛できるので右辺値、左辺値についてconst lvalue参照できます
以上がテンプレートの型推論規則です
###明示的な型指定
明示的に型を指定した場合はどうでしょう
明示的に型をわたして関数テンプレートを使用するということは
型推論とは別の規則が適用されるということです
以下のコードはどのように扱われるのでしょうか?
template < typename T >
void f( T&& ) ;
int x = 2 ;
f<const int>(x) ;
結論からいうとこのコードはコンパイルエラーになります
順をおって説明すると
明示的に型を指定すると指定された型がそのままT
に適用されます
つまり、Tはconst int
であり
パラメータタイプはconst int&&
となります
右辺値参照は左辺値を束縛できないのでエラーになります
この例では直感的にはT
がconst int
になり, パラメータタイプをユニヴァーサル参照の規則でconst int&
とすることでconst性を強制しようとしていますが、失敗します
ユニヴァーサル参照の最大の利点は__ムーブ・セマンティクス__が使える点でしょう
const性を強制するとムーブ・セマンティクスが使えないためconst T&
を使うべきです
更に危険なコードを書くことができます
template < typename T >
void f( const T& x ){
x = 0 ;
}
int x = 2 ;
f< int& >( x ) ;
このコードはconst指定されているはずの変数x
を書きかえています
コンパイルエラーになってほしいです
が!
残念なことに(?)コンパイルでき、変数を書きかえることに成功します!
参照型を明示的に関数テンプレートにわたしたときには
パラメータタイプレベルでのcv修飾は無視されれるのです
まあ、こんな意味不明な使い方をする人はいないかもしれません
##跋文
この記事を書いている間にも、どんどん躓いたため
非常に長い記事になってしまいました
最後まで読んでくださった方はありがとうございます
おかしな箇所があれば、ご指摘ください
明日は、okdshinさんの記事です。
##参考文献
『新改訂版 C++ポケットリファレンス』
(高橋晶、安藤敏彦、一戸優介、楠田直矢、道化師、湯朝剛介 著、株式会社技術評論社、ISBN 978-4-7741-7408-2)
『Effective Modern C++ ―C++11/14プログラムを進化させる42項目』
(Scott Meyers 著、オライリー・ジャパン、ISBN 978-4-87311-736-2)