LoginSignup
181
180

More than 1 year has passed since last update.

C++のつまずきポイント解説 その1

Last updated at Posted at 2015-12-11

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は一意でなければならないのでaint, bdoubleに推測されるこのコードはill-formedになります

autoとtemplateの違い

auto{}で初期化した場合には必ずstd::initializer_listであると推測します
対してテンプレートパラメータTstd::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

この関数の推測は、引数を未知の型Tinitializer_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を用いるのがよいです

値の高速な検索を必要とする場合はmapunordered_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 ハッシュによる重複可能な集合(多重集合) 非順序連想コンテナ
シーケンスコンテナ
要素の順序が維持されるコンテナ
連想コンテナ
要素が整列されて格納されるコンテナ
非順序連想コンテナ
要素に順序がなく、ハッシュを用いて管理されるコンテナ
コンテナアダプタ
実際にはコンテナではなく、他のコンテナへの操作を一部制限する形でデータ構造を表現するコンテナ

コンテナとイテレータ

イテレータとはコンテナとアルゴリズムを仲介するインターフェイスです
1. operator*で値を取り出せる
2. 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(パラメータタイプが参照でもポインタでもない)

仮引数が参照でもポインタでもない場合は値渡しになり、その場合の規則は
1. cv修飾は無視される
2. 参照は無視される
となります


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 <class T> int f(T&&);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
// would bind an rvalue reference to an lvalue

—end example ]

要約すると、仮引数がcv修飾されている場合には推論の段階で引数の型のcv修飾は無視されます
その後、参照も無視されて型推論されます

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&&となります
右辺値参照は左辺値を束縛できないのでエラーになります

この例では直感的にはTconst 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)

N3777
N4140

181
180
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
181
180