C++初心者Advent Calendar 2015
この記事はC++初心者Advent Calendar 2015 17日目の記事です
<<9日目 |Clang with Microsoft CodeGenがでたので試す
<<16日目 | ブログズミ: Boost.Test v3 を使ってみた](http://srz-zumix.blogspot.jp/2015/12/boosttest-v3.html) || [18日目 | それC++なら#defineじゃなくてもできるよ | とさいぬの隠し部屋 >>
はじめに
みなさま、ナマステ。さて、この記事のタイトルを見て、「時代・・・サトウキビ・・・忍者・・・うぅ、頭が」となった人は私と趣味が似ています・・・って話はどうでもいいか。
よく、「C++はBetter Cとしてはじめればいい」みたいな話を聞くのでそれにそって一気にC++14まで駆け抜けようと思います。
ちなみにC99を知らない人はお断りです。さようなら。
でははじめますか。
しっかし書き終わってから思った。なぜ複数の記事に分割しなかったし
というわけで、超長文です。すみません。そのうち単独の記事も出します、きっと。
ところで、実はホッとしておりまして、なにかというと、前日のsrz-zumixさんの記事がちっとも初心者向けじゃないんですよ。初心者はBoostなんて使えません!!
書き終わってから投稿に気が付いたんですが。この記事の後半は初心者向けじゃないなぁと思っていたのですが、それみて「これよりは初心者向けだわー」といった感じです。
個人的には、歌舞伎座.tech #8 「C++初心者会」とか見て以降、「初心者」が怖くなってしまっていましたが(初心者がBoost.asioをつかえるもんか!)、さらに怖くなりました。初心者怖い。(srz-zumixさんすみません)
=0で初期化をやめよう
0初期化は妥当か?
さて、初期化と聞くと
typedef struct {
int a;
int b;
} Hoge;
static int n;//static/thread_localな変数は0初期化される
int main(void)
{
int foo = 0;//変数fooを0初期化
Hoge hoge = { 0 };//変数hogeの最初のメンバ変数aを0で初期化、それ以外のメンバ変数(=メンバ変数b)をstatic変数と同じ方法で初期化(=0初期化)
//略
return 0;
}
みたいに、0初期化を浮かべる人が多いかもしれませんが、初期化はそれだけではありません。
そもそも初期化とは、変数などの状態をプログラマにとって既知にすることで、断じて0にすることと同義ではありません。どういうことでしょう?
#include <stdlib.h>
int main(void)
{
int foo = 2;//変数fooを2で初期化
int* n_p = malloc(sizeof(int));//変数n_pをmalloc関数で割り当てたメモリー領域へのポインタで初期化
memset(n_p, 0xcc, sizeof(int));//変数n_pをmalloc関数で割り当てたメモリー領域を0xCCで全byte埋めて初期化
free(n_p);
return 0;
}
一般に初期化は、変数の読み出し操作の前に、変数の状態をプログラマーから既知にするために行います。static/thread_localな変数以外の変数は初期化子を書かない場合デフォルト初期化されますが、この時、クラス型ではないもしくは配列型で要素型がクラス型ではない変数(ex.)int型のような組み込み型やそのポインタ型のint*型、int[5]のような配列型)は初期化が行われません(=値が不定)。
値が不定だと困るので初期化子を書くわけですが、この時0初期化することが多いの、はxor命令に帰結させたりmemsetの呼び出しに最適化できたりするために一般に高速なためであって、なにも0でなくとも初期化には違いないわけです。
なお
typedef struct {
int a;
int b;
} Hoge;
int main(void)
{
Hoge hoge;
memset(&hoge, 0, sizeof(Hoge));
return 0;
}
のようにするコードをたまに見かけますが、わざわざmemsetを自分で書かなくても
typedef struct {
int a;
int b;
} Hoge;
int main(void)
{
Hoge hoge = { 0 };
return 0;
}
で十分です。現代のコンパイラはmemsetの呼び出しに最適化できます。
またC++の場合は
struct Hoge {
int a;
int b;
};
int main()
{
Hoge hoge = {};
return 0;
}
で十分だったりします。
=0で初期化できない例
=0と書く初期化は次のようなものです。
int main(void)
{
int n = 0;
return 0;
}
しかしこれはCなら構造体、C++ならクラスには適用できません
typedef struct {
int a;
int b;
} Hoge;
int main(void)
{
//Hoge hoge = 0;//NG
return 0;
}
0初期化するべきではない場面
memsetで0初期化もよく見かけます。これはどうでしょうか?
typedef struct {
int a;
int b;
} Hoge;
int main(void)
{
Hoge hoge;
int n;
memset(&hoge, 0, sizeof(Hoge));
memset(&n, 0, sizeof(int));
return 0;
}
C99では問題ありませんでした。しかしC++にはクラスがあります。
ここで次のようなクラスを見てみましょう。
#include <cstring>
#include <iostream>
#include "hexdumper.hpp"
class Hexa {
public:
Hexa() : str_("arikitari") {}
~Hexa() = default;
virtual void setStr(void){}
protected:
char str_[0x10];
};
class Hexa2 : public Hexa {
public:
Hexa2() : Hexa() {}
~Hexa2() = default;
void setStr(void) noexcept override { std::strcpy(this->str_,"hexadrive"); }
char* drawStr(void) noexcept { return this->str_; }
const char* drawStr(void) const noexcept { return this->str_; }
};
int main(void)
{
using std::endl;
Hexa2 hexa = {};
std::cout << "before memset" << endl << hexdump(hexa) << endl;
Hexa2* pHexa = &hexa;
std::memset(pHexa, 0, sizeof(Hexa2));
std::cout << "after memset" << endl << hexdump(hexa) << endl;
pHexa->setStr(); // ここでクラッシュする
std::cout << pHexa->drawStr() << std::endl;
}
hexdump
は自作関数です。
仮想関数がある派生クラスに対してmemsetを用いた例です。仮想関数であるsetStr
は呼び出せず、手元の環境ではSegmentation faultしました。なぜでしょうか?
before memset
Address | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 123456789ABCDEF
---------+-------------------------------------------------+----------------
00000000 | 00 19 40 00 00 00 00 00 61 72 69 6B 69 74 61 72 | .@.....arikitar
00000010 | 69 00 00 00 00 00 00 00 | i.......
after memset
Address | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 123456789ABCDEF
---------+-------------------------------------------------+----------------
00000000 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
00000010 | 00 00 00 00 00 00 00 00 | ........
Segmentation fault
memsetの前後でclass変数を自作したhexdump関数でダンプしてみました。このコードの処理系では、仮想関数をvtableと呼ばれるもので実装しているようで(というかそれ以外の実装あるの?)、00000000
~00000007
の範囲がどうも該当しているようです。memsetによってこの部分が吹き飛ばされてしまうため、仮想関数であるsetStr
の実行コードのあるアドレスが消え、結果としてヌルポしているようです。
また、コンストラクタでメンバ変数str_
に与えたarikitari
という文字列も吹き飛んでいます。せっかくデフォルトコンストラクタで初期化したのに、memsetで2重の初期化をしてしまっているわけです。
クラス変数の初期化memsetを使いたい場合は最低でもstandard-layout class(trivially copyable classである必要はあったっけ?)、一般にはPODクラスであるべきで、PODとは何かがわからないうちは、memsetをクラス変数に使うのはご法度とおぼえておくべきです。
統一初期化構文(Universal Initialization/Uniform Initialization)で初期化しよう
じゃあどうすればいいのでしょうか。
- 統一初期化・リスト初期化の見本帳 - イグトランスの頭の中
- 波括弧の初期化があって嬉しいとき - イグトランスの頭の中
- aggregateと初期化リストの不思議 | 本の虫
- 多くのプログラマは言語を表面的な理解だけで使っている | 本の虫
- initializer listの解説
- C++14の新機能: メンバー初期化子と初期化リストの組み合わせ | 本の虫
- vector/arrayとUniform initialization+Initializer list - yohhoyの日記
- C++11: Syntax and Feature#8.5 初期化子(Initializers)
はい、2日目、3日目でも解説がありましたし、本の虫にもありますね。統一初期化構文(Universal Initialization/Uniform Initialization)を使えばいいです。
std::pair<int, int> p3{};//OK
int hoge{};//OK
std::array<int, 4> arr{{}}
のようにかけます。
後述するtemplateと組み合わせて、任意の型をstatic storageと同じように初期化したいときとかは統一初期化構文一択だったりします。
まあしかし、実際の運用では常に使うというわけではなく、使うべきでない場面というのもあります。
C++11 Universal Initialization は、いつでも使うべきなのか
を読んでみると良いでしょう。
std::coutとprintf
C++でなんか出力と言われたらやっぱり
#include <iostream>
int main()
{
std::cout << "arikitari_na_world!" << std::endl;
return 0;
}
ですよね。なんでprintfが好まれないのか見ていきましょう。
そもそもprintfのプロトタイプ宣言は
int printf ( const char * format, ... );
ですね。
format指定と型安全
例えばstdint.h
/cstdint
にあるint64_t
型を表示させたい場合、どうすればいいでしょうか?
#include <stdint.h>
#include <stdio.h>
int64_t num = 4288957324576;
//どっち?
printf("%lld", num);
printf("%I64d", num);
この2つを#if
つかって分けるというコードを書かねばならないのでしょうか?
もちろんそんなことはなく、標準にそれをやってくれるマクロがあります。#define __STDC_FORMAT_MACROS
& #include <inttypes.h>
(C99) / #include <cinttypes>
(C++11) のマクロをつかって、
#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>
int64_t num = 4288957324576;
printf("%" PRId64, num);
こう書くことはできますが、文字列が(マクロで連結されますが)とぎれて見えます。これはなかなかみにくいんじゃないでしょうか(私見)。ちなみに、文字列リテラルとマクロの間にスペースを入れ忘れると、C++11の機能であるUDLsと誤認されます。
問題はさらに深いです。format指定することの利点は出力されるものがわかりやすいということになりますが、対応させるべき型が無限に存在する場合、無限通りのformatが必要になります。恐ろしや。STLに入っている型のうち対応させるべきものだけでもいくつあるんですかね、覚えられるか。
さらに型安全なものにしようとしたら、つまり、format指定が適切かを調べコンパイルエラーにするには、どれだけ苦労しなければならないのでしょうか。先のようなマクロもたくさん必要になります。
それでも型安全printfをつくるべく立ち向かう人たちは後を立ちません。
結論
#include <iostream>
#include <cstdint>
#include <string>
int main()
{
std::int64_t num = 4288957324576;
std::size_t s = 32;
std::string str = "arikitari";
std::cout << num << ", " << s << ", " << str << std::endl;
return 0;
}
templateが使えるおかげで、膨大な型全てに対応できました。formatがあるから面倒なんだ、という論法ですね。
ただし、iostreamいいぞ、という書き方をしましたが、正直に言うと設計が古すぎて、作り直すべきという声もC++聞こえてきます。
ああ、ちなみにfmtlib/fmtというライブラリがあって、高速かつC#に近いSyntaxで書けます。
C++ Advent Calendar 2014の17日目の記事である
今年気になった C++ ライブラリとかフレームワークを紹介する記事 - はやくプログラムになりたい
に紹介があります。2017/02/02現在も活発に開発されているようです。
fmt::print("Hello, {}!", "world"); // => Hello, world!
std::string s = fmt::format("{0}{1}{0}", "abra", "cad");
// s == "abracadabra"
auto
いわゆる型推論ですね。どこぞの糞言語(Java)を除けば、型推論は右辺の型から左辺の型を推測するものです。
int num = 2;
auto re = 5 / num;//int型、2になる
組み込み型くらいはauto使わないほうがいいですが、std::array::iterator
とかstd::vector<std::vector<int>>::iterator
とかわけわかめな長さの型が使われるので必須機能です。
あ、あとTMPする時・・・(ry
この辺はいなむ先生が、C++アドベントカレンダー12日目で
C++のつまずきポイント解説
詳しく書いているので、少し難し目ですがぜひ読んでみてください。
std::pair
さて、こいつはなにかと便利です。純粋に2つの変数をまとめるだけのものです
typedef struct {
int first;
int second;
} PAIR;
みたいなものを二度と書かなくて良くなります。2つペアのものって多いですからね。座標とか、画像の大きさとかetc...
専用のクラスを書くまでもないときに重宝します。
#include <string>
#include <utility>
#include <numeric>
#include <stdexcept>
#include <cctype>
int calc_check_digit(const std::string& n) noexcept(false) {
if (11 != n.size()) throw std::runtime_error("n.digit must be 11");
const int r = std::accumulate(n.rbegin(), n.rend(), std::pair<int, int>{}, [](const auto& s, const char& e) -> std::pair<int, int>{
if(!std::isdigit(e)) throw std::runtime_error("n.digit must be 11");
return {s.first + (e - '0') * ((5 < s.second) ? s.second - 4 : s.second + 2), s.second + 1};
}).first % 11;
return (0 == r || 1 == r) ? 0 : 11 - r;
}
firstを合計、secoundをループカウントに使いました。
std::array
配列です。説明は以上です。
・・・嘘です。
Cにも配列があって
#include <stdio.h>
#include <stdlib.h>
#ifndef _countof
#define _countof( arr ) ( sizeof(arr) / sizeof(arr[0]))
#endif
int main()
{
int arr[3] = { 3, 4, 2 };
for(size_t i = 0; i < _countof(arr); ++i) printf("%d,", arr[i]);
return 0;
}
こんなコードを書いたことはあると思います。ちなみにこれC++だと
#include <iostream>
int main()
{
int arr[3] = { 3, 4, 2 };
for(auto it = std::begin(arr); it != std::end(arr); ++it) std::cout << *it << ", ";
return 0;
}
こう書けます。std::begin
とstd::end
が配列にも使えるんですね。
std::array
を使うと
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> arr = { 3, 4, 2 };
for(auto it = arr.begin(); it != arr.end(); ++it) std::cout << *it << ", ";
return 0;
}
で、Cの配列ではなくstd::array
を使うメリットは、
-
back()
(最後の要素を取得)やsize()
(配列の大きさを取得)が使える - 式中でポインタとみなされない
式中でポインタとみなされないとはどういうこっちゃ?ですが、例えばCで要素型がint型の配列の全要素を表示する関数を作ることを考えてください。
#include <stdio.h>
void array_print(const int * arr, size_t size)
{
for(size_t i = 0; i < size; ++i) printf("%d, ", arr[i]);
}
Cでは御存知の通り、配列は3つの例外を除き、常にポインタに読み替えられます。3つの例外の1つはsizeof
演算子を使う場合ですが、
#include <stdio.h>
void f(int arr[])
{
printf("%d", sizeof(arr));//arrはint*型
}
int main()
{
int arr[5];
printf("%d", sizeof(arr));//arrはint[5]型
f(arr);
return 0;
}
このように、要素数の型データが消えてしまうわけです。
C++を使えばもうすこしマシに書けます。
#include <iostream>
#include <cstddef>
void f(const int (&arr)[5])
{
std::cout << sizeof(arr);//arrはconst int (&)[5]型
}
int main()
{
int arr[5];
std::cout << sizeof(arr);//arrは//arrはint[5]型
f(arr);
return 0;
}
しかし、参照として渡されてもそれを取り回すのがわりと面倒です。また戻り値として指定できないことには代わりありません。
そこでstd::array
です。
#include <iostream>
#include <array>
void f(const std::array<int, 5>& arr)
{
std::cout << arr.size();
}
int main()
{
std::array<int, 5> arr;
std::cout << arr.size();
f(arr);
return 0;
}
すっきり。
Cを長くやっていた人なら、構造体のなかに配列のみをもたせたものを作った経験があるかもしれませんが、まさにそういう実装になっています。
なおsize()
は明日のmyon___氏が記事で書いてくれるかもしれませんが、constexpr
関数なのでコンパイル時に値が決定します。どこぞの陶芸家で中3女子な人が喜びそう。
Range-based for
Range-based forとはCの配列と、begin()
/end()
メンバ関数をもつクラス型と(特殊な?)ADLでbegin()
/end()
関数が見つかる型にのみ提供できるfor文です。いわゆるforeachですね。
#include <iostream>
int main()
{
int arr[3] = { 3, 4, 2 };
for(auto it = std::begin(arr); it != std::end(arr); ++it) std::cout << *it << ", ";
return 0;
}
これが
#include <iostream>
int main()
{
int arr[3] = { 3, 4, 2 };
for(auto&& e : arr) std::cout << e << ", ";
return 0;
}
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> arr = { 3, 4, 2 };
for(auto it = arr.begin(); it != end(); ++it) std::cout << *it << ", ";
return 0;
}
これが
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> arr = { 3, 4, 2 };
for(auto&& e : arr) std::cout << e << ", ";
return 0;
}
こう書けます。auto&&
ってのはRange-based_forを使う際のおまじないです。ループ内でループ対象を書き換えないならconst auto&
のほうがいいかもしれませんが、前者の方が汎用的なのでよくわからない時は前者を使えばいいと思います。おまじないの原理は
range-based for loopsの要素の型について
とコメントを参照してください。この下で話すReferenceの種類と参照できるものが関係します。
Reference
参照、と言ったほうが聞いたことがあるかもしれません。Referenceとはすでにある値に対し別名をつける機能です。
で、Referenceですが、大きくわけてlvalue referenceとrvalue referenceがあります。
で、この2つにどんな差があるのか、ですが、参照できるものの型を除けばなにも違いはありません。よくrvalue referenceはlvalue referenceと全く異なる、と考えて違いを考えすぎるあまり、わけわかめになる人がいますが、Referenceには違いないのです。
種類 | 参照できるもの |
---|---|
lvalue reference(T&) | lvalue |
const lvalue reference(const T&) | なんでも |
rvalue reference (T&&) | rvalue |
const rvalue reference(const T&&) | const rvalue |
struct Test{
int e;
};
const Test make_Test(){ return Test(); }
int main()
{
int a = 0;
const int b = 2;
Test t;
int& a_lr = a;
const int& a_clr = a;//OK
//int& b_lr = b;//NG
const int& b_clr = b;//OK
Test& t_lr = t;//OK
Test&& t_rr = Test();//OK
const Test& t_clr = Test();//OK
const Test&& t_crr = make_Test();//OK
const Test& t_clr = make_Test();//OK
return 0;
}
という感じで、const lvalue reference
が無双というか最強なので、rvalue referenceはあまり出番がありません。
rvalue referenceをムーブに使う
追記: さらに明快な解説を書きました
みんなlvalueとrvalueを難しく考えすぎちゃいないかい?
さて、ざっとconstの有無を含めて4つのreferenceがあったわけですが、rvalue referenceに出番が無いかというとそんなことはありません。まあ他にもありますが、最も一般的な例であるrvalue referenceの用途、moveを紹介します。
実質的な機能は同じでも型は違います。また、それぞれ優先順位があります。ということは、関数のオーバーロードで型を変えれば呼び分けができるとうことです。
優先順位の詳細は
const rvalue referenceは何に使えばいいのか - ここは匣
を参照していただくとして、簡単な例を見ましょう。・・・クラスがなにかの説明は省きます。
#include <cstring>
#include <cstddef>
#include <iostream>
class inferior_string
{
public:
inferior_string() noexcept : m_s_(nullptr), m_len_(0), m_capacity_(0) {}
inferior_string(const char* str)
{
const std::size_t len = (nullptr == str) ? 0 : std::strlen(str);
if(0 == len){
this->m_s_ = nullptr;
this->m_len_ = this->m_capacity_ = 0;
}
else{
const std::size_t cap = 2 * len;
this->m_s_ = new char[cap]();
std::memcpy(this->m_s_, str, len);//copy
this->m_len_ = len;
this->m_capacity_ = cap;
}
}
inferior_string(const inferior_string& o)//copy constructor
{
if(0 == o.m_len_){
this->m_s_ = nullptr;
this->m_len_ = this->m_capacity_ = 0;
}
else{
const std::size_t cap = o.m_len_ * 2;
this->m_s_ = new char[cap]();
std::memcpy(this->m_s_, o.m_s_, o.m_len_);//copy
this->m_len_ = o.m_len_;
this->m_capacity_ = cap;
}
}
inferior_string(inferior_string&& o) noexcept
: m_s_(o.m_s_), m_len_(o.m_len_), m_capacity_(o.m_capacity_)//move constructor
{
o.m_s_ = nullptr;//disable input object's destructor. DO NO FORGEET!!!
}
~inferior_string()
{
delete[] this->m_s_;
}
const char* c_str() const noexcept { return this->m_s_; }
private:
char* m_s_;
std::size_t m_len_;
std::size_t m_capacity_;
};
std::ostream& operator<< (std::ostream& os, const inferior_string& str){
os << str.c_str();
return os;
}
int main()
{
inferior_string str = "arikitari";
inferior_string str2 = str;//copy constructor call
std::cout << str << ", " << str2 << ", ";
inferior_string str3 = std::move(str);//move constructor call
//inferior_string str3 = static_cast<inferior_string&&>(str);//同じ意味
std::cout << str3 << std::endl;
return 0;
}
この場合、const inferior_string&
よりinferior_string&&
のほうが、オーバーロードの優先順位が高いので、
inferior_string str3 = static_cast<inferior_string&&>(str);
これはinferior_string&&を受け取る、move constructorが呼ばれます。ただし、このキャストを書くのはだるいので
inferior_string str3 = std::move(str);
と書くのが一般的です。
こうしてみてわかったように、断じてrvalue reference自体にはmove機能はありません。たかが参照に一体何を求めてるのさ。
この辺もいなむ先生が、C++アドベントカレンダー12日目で
C++のつまずきポイント解説
詳しく書いているので(ry
念の為に引用しておきます
http://cpplover.blogspot.jp/2009/12/rvalue-reference.html
std::move()を、何かコア言語の機能のように勘違いしていませんか?
あくまでSemanticsです。
std::moveは、static_cast(a) をしているにすぎないのです。
rvalue referenceも、単なるreferenceに過ぎないのです。
Move Semanticsとは、たんにlvalueとrvalueを、movableなフラグとして使っているに過ぎないのです。
その他は、lvalue referenceの場合と、何も変わりありません。
lvalue referenceで、データメンバにアクセスしたからと言って、そのオブジェクトがその後使えなくなるとは限らないでしょう。
もちろん、参照しているわけですから、publicなメンバ変数に対して、破壊的な書き換えもできるわけです。
単にlvalueとrvalueを、movableなフラグとして使っているに過ぎないんです。
std::string
さきほどなんちゃって文字列クラスを作りましたが、ちゃんとC++にはstd::string
があるのでそれを使いましょう。
というかCで文字列操作するな、そういうことする言語じゃない!!
#include <iostream>
#include <string>
int main()
{
using std::cout;
using std::endl;
std::string str1 = "arikitari_na_world!";
cout << str1 << endl;
str1.popback();//最後の一文字消去
cout << str1 << endl;
const auto str2 = str1.substr(0, str1.find_first_of('_'));//最初の'_'より前を抜き出し('_'は含めない)
cout << str2 << endl;
const auto str3 = str2 + "_toha";//文字列の連結
cout << str3 << endl;
return 0;
}
・・・まあこんな感じで使えます。例えばフルパスからファイル名だけほしい時は
#include <iostream>
#include <string>
std::string get_n(const std::string& fullpath)
{
return fullpath.substr(fullpath.find_last_of("\\/"), fullpath.find_last_of('.'));
}
int main()
{
using std::cout;
using std::endl;
std::string str1 = "C:\\Users\\yumetodo\\OneDrive\\ドキュメント\\東京理科大\\物理学実験\\fit.log";
cout << str1 << endl;
cout << get_n(str1) << endl;
return 0;
}
こんな感じですね。とっても楽。
もっと初心者向けな説明は
22日目| C++の文字列処理関係と正規探索(未完)について - 水面下の夢
へ。
template入門
さきに「初心者にはtemplateなんて無理です」とか言ったのは誰でしょうね(私だ)。
大丈夫です、入門です。んなのまじめに解説したら本が一冊書けます。以下の解説より詳しく知りたい人は、
C++関数テンプレートと半順序とオーバーロード
を見てください。いなむ先生がなぜかプロ生ちゃんのAdventCalenderに投稿してます。
まあすでに出てきましたが。まずは定義から。
テンプレートとは、コンパイル時に型や値を引数として渡す機能のことである。
14 テンプレート(Templates) | C++11の文法と機能(C++11: Syntax and Feature)
分かった・・・?いい?コンパイル時だよ?コンパイル時。そこ大事だからね。
#include <iostream>
template<typename T_>
constexpr const T_ & max(const T_& a, const T_& b)
{
return (a > b)? a : b;
}
int main()
{
const auto hoge1 = max(54, 23);//hoge1はint型
const auto hoge2 = max<unsigned int>(54, 23);//hoge2はunsigned int型
std::cout << hoge1 << hoge2 << std::endl;
return 0;
}
みれば分かるように最大値を返す関数ですが、型が「T_」になってます。どういうことだってばよ?
これまで最大値を求める関数を作ろうと思ったら全部の型ごとに関数を書く必要が有りました。事実C言語のmath.hを見ると同じような機能の型が違う関数が乱造されています。んなもんいちいち覚えてらんないですよね?
そういった背景から(?)C++では関数をオーバーロード出来るようになりました。つまり、引数の型が異なれば同名の関数をいくつでも作れるようになりました。
2行目を見てください。templateから始まる部分がありますが、これがtemplateの仮引数と呼ばれるところです。関数にも仮引数があったけどあれに似てます。
ただし指定できるのは型名(とコンパイル時定数)のみです。
例えばこの場合新たにT_という型をでっち上げているわけですが、この時点では実際の型はわかりません。若干違いますが型の異なる関数が無限に存在するイメージで差し支えありません(但し、コンパイル時に確定します)。
7行目を見てください。これはテンプレートの実引数推定(Template argument deduction)という機能を使っています。
できるだけ普通のプログラマーの常識に合わせるために、とても複雑になっているのですが、逆に言えば、
ノリと勘と気分となんとなくでどーにかなるということです。
ようはtemplate関数の引数に書いた型に推論されるわけで。まあみればわかるでしょ。
8行目は明示的なテンプレート実引数指定(explicit template argument specification)と呼ばれていて、かっこ良く名前をつけましたがこの場合ならようはT_の型はunsigned intだよ~と教えているだけです。
7, 8行目のようにtemplateを使ったもの(今回はtempalte関数)を実際に読んでコンパイルされると、先ほどの「無限に存在する」状態の例えで言うならどれか特定のものに定まります。
もちろん実際に関数としてコンパイルされるのは呼び出されているものだけです(つまり使う場所と同じ翻訳単位にないとうまくいかない)。
初心者だってstd::enable_ifでSFINAEしたい
C++関数テンプレートと半順序とオーバーロード
SFINAEとは
Substitution Failure Is Not A Error
の略語である
知らんかったわ、まあそれはさておき。
テンプレートはどんな型でも受け取っていまいます。そのままだと予期せぬ動作をしたり、「コンパイルエラーの爆発量を競う大会」が開かれるほどのエラーメッセージがでます。
使える型を制約するにはどうしたら良いでしょうか?
conceptの歴史(超要約)とC++1z(C++17)
ここでこの記事のタイトルを改めてみましょう。「C99からC++14を駆け抜けるC++講座」です。残念ながらC++1z(C++17)の機能は紹介できないわけです。
なんでこんなことを書くかというと、C++1zにconceptなる機能が提案されています。もともとC++11で入るはずのものでしたが、
Bjarne Stroustrup、Conceptと未来を語る | 本の虫
にあるように、かなりいろいろあって結局入りませんでした。それからまもなく6年、再びconceptが提案されています。
新機能"コンセプト"でC++1z時代のジェネリックプログラミング
早くほしいです。これから紹介するstd::enable_ifよりも直感的なはずですから。
追記:こんないい加減な説明より、江添さんの
帰ってきたコンセプト | Boost勉強会 #16 大阪
が数千倍わかりやすいです。(岡山の陶芸家(@bolero_MURAKAMI)とでちまるさんの兄(@decimalbloat)はワロタ)
しかし無いものは仕方ないです。std::enable_ifを紹介しましょう。
更に追記:どうもC++17に提案されていたconceptはrejectされたっぽい・・・?C++11でもさんざん揉めて入らなかったのにまた入らねーのかよ!
C++17のif constexpr
とC++14の戻り値にautoを使う記法で少しましになりそうですが
#include <type_traits>
template<typename T>
auto f(const T& a){
if constexpr(std::is_integral_v<T>){
return a + 1;
}
else if constexpr(std::is_floating_point_v<T>){
return a + 0.1;
}
else {
return a;
}
}
int main(){
using namespace std::literals;
[[maybe_unused]] auto a1 = f(1);//result: 2(int)
[[maybe_unused]] auto a2 = f(1.0);//result: 1.1(double)
[[maybe_unused]] auto a3 = f("arikitari"s);//result: "arikitari"(std::string)
}
いや、conceptくれ。
std::enable_if
std::enable_if
なるものがありまして、これを使うとSFINAEを悪用利用した型制約templateを書けます。
いろいろ流儀がありますが、私は
std::enable_ifを使ってオーバーロードする時、enablerを使う?
の方法が好きなのでそれを紹介します。enablerを使わず、std::nullptr_t
を使うといえば伝わる人には伝わるでしょう。
例えば、算術型(intとかdoubleとか)だけを受け取りたい場合を考えましょう。
まずは算術型か否かを判別する必要があります。
そういう時に活躍するのがtype_traitsヘッダーです。ていうかstd::enable_if
もこれincludeしないと使えません。
で日本語より英語のサイトのほうがわかりやすいので
<type_traits> - C++ Reference
を見ながら話を進めます。
こんな風に書いてありますね。算術型はarithmeticって言うんですね・・・ってそうじゃない。
つまりstd::is_arithmetic
をつかえばいいと分かります。
is_arithmetic - C++ Reference
std::is_arithmetic<T>::value
これがtrueになる時、T
は算術型ですね。
さて、であとはstd::enable_if
を書くだけですが、その前におまじないを。typename
って書くのはだるいので。
namespace std{
template<bool condition, typename T = void>
using enable_if_t = typename std::enable_if<condition, T>::type;
}
std::enable_if_t
ってのが標準にあるんですね。
では算術型のみ受け取る関数fを作ってみましよう。
#include <iostream>
#include <type_traits>
template<typename T, std::enable_if_t<std::is_arithmetic<T>::value, std::nullptr_t> = nullptr>
void f(T num)
{
std::cout << "num:" << num << std::endl;
}
int main()
{
f(3);
//f("num");
return 0;
}
もしコメントアウトを外すと
prog.cc:12:5: error: no matching function for call to 'f'
f("num");
^
prog.cc:3:48: note: candidate template ignored: disabled by 'enable_if' [with T = const char *]
using enable_if_type = typename std::enable_if<condition, T>::type;
^
1 error generated.
のようにコンパイルエラーになります。やったぜ!
さて、ここからがSFINAEの出番です。
#include <iostream>
#include <type_traits>
template<typename T> struct is_char_type : public std::false_type {};
template<typename T> struct is_char_type<T const> : public type_traits::is_char_type<T> {};
template<typename T> struct is_char_type<T volatile> : public type_traits::is_char_type<T> {};
template<typename T> struct is_char_type<T const volatile> : public type_traits::is_char_type<T> {};
template<> struct is_char_type<char> : public std::true_type {};
template<> struct is_char_type<wchar_t> : public std::true_type {};
template<> struct is_char_type<char16_t> : public std::true_type {};
template<> struct is_char_type<char32_t> : public std::true_type {};
template<typename T, std::enable_if_t<std::is_arithmetic<T>::value, std::nullptr_t> = nullptr>
void f(T num)
{
std::cout << "num:" << num << std::endl;
}
template<typename T, std::enable_if_t<is_char_type<T>::value, std::nullptr_t> = nullptr>
void f(const T* s)
{
std::cout << "str:" << s << std::endl;
}
int main()
{
f(3);
f("num");
return 0;
}
こんな風に文字列へのポインタを受け取るオーバーロードを追加しました。
もしSFINAEがないとf(3)
という呼び出しはコンパイルエラーになります。なぜならば、オーバーロード解決のためにまずfという名前の関数を捜索し、2つ見つかるわけですが、下のほうのfが呼べるかを調べるときにエラーになるからです。
SFINAEはこのエラーを**とりあえず無視(=オーバーロード候補から外す)**ので関数が一つに定まり、呼び分けができるわけです。
ちなみにC++03でも仕様が曖昧だっただけで使えたコンパイラもあったらしいです
任意の式によるSFINAE - cpprefjp C++日本語リファレンス
まとめると、型名T
に制約を書ける場合は
template<typename T, std::enable_if_t<許可条件(trueで有効), std::nullptr_t> = nullptr>
のように書けばいいということになります。
実際にこれを使って、2次元のpointクラスを作ってみたものがこちらになります。
https://github.com/Nagarei/DxLibEx/blob/master/dxlibex/basic_types/point2d.hpp#L72
型制約をかけただけではSFINAEは使っていません、オーバーロード解決の際の候補から外すことがSFINAEです、念のため
templateでif
もはや初心者とは何だったのかという内容ですが、さわりだけ。
結論から言うと、C++11でstd::conditional
が入りまして、まんまIFなので、それ使えばいいです
メモ:std::conditionalでif~else if~elseみたいなことをしようとすると見づらい
C++0x std::conditional - Faith and Brave - C++で遊ぼう
//C++11 or later
using c_type = typename std::conditional<[(コンパイル時に評価できる)条件式], [真の時の型], [偽の時の型]>::type;
//C++14 or later
using c_type = std::conditional_t<[(コンパイル時に評価できる)条件式], [真の時の型], [偽の時の型]>;
のように書けまして、
Variadic Template を使って switch を使ったテンプレート関数呼び出しを除去する
C++ Advent Calendar 2015の2日目の記事ですがこんな具合に悪用利用できます
ですが原理を説明しましょう。
#include <type_traits>
template <bool Con, class Then, class Else>//(2, 3, 4)
struct IF;//(1)
template <class Then, class Else>//(5)
struct IF<true, Then, Else> {//(6)template第一引数がtrueの時はこの定義
typedef Then type;//(7, 8, 12)
};
template <class Then, class Else>//(5)
struct IF<false, Then, Else> {//(6)template第一引数がfalseの時はこの定義
typedef Else type;//(7, 9, 15)
};
//中略
using type1 = typename IF<true, int, double>::type;//int型になる(10, 11, 13)
using type2 = typename IF<false, int, double>::type;//double型になる(10, 14, 16)
まるで暗号のようだという声が聞こえてきそうです。一つ一つ説明します。
- まず、
IF
というクラス(structと書いてあるので原則public指定)があります。 - こいつはtemplateクラスです。
- このクラスのtemplate引数は3つです
- 一つ目は
bool
型、2つめと3つめは型名です - 2つのtemplate特殊化があります
- template第1引数がtrueの時とfalseの時です
- 2つのtemplate特殊化ではどちらでも型名typeがtypedefされます
- template第1引数がtrueの時、型名typeはtemplate第2引数の別名となります。
- template第1引数がfalseの時、型名typeはtemplate第3引数の別名となります。
- このクラスを使ってみます。
- template第1実引数にtrueが指定されました
- 型名typeはtemplate第2実引数の別名になるのでint型になります
- それの別名として
type1
という新たな型を定義するので、type1
はint型です - template第1実引数にfalseが指定されました
- 型名typeはtemplate第3実引数の別名になるのでdouble型になります
- それの別名として
type2
という新たな型を定義するので、type2
はdouble型です
説明とコード中の()の数字を対応させながら読んでみてください。
で、今回trueとかfalseとかやってたところはもちろん、std::is_arithmetic
とか、とにかくコンパイル時に値が決まれば指定できます。
templateをつかって入力関数を作ってみよう
追記
単体の記事にしました
http://qiita.com/yumetodo/items/2a1d5f855bae6d100658
License
C++初心者Advent Calendar 2015
この記事はC++初心者Advent Calendar 2015 17日目の記事です
<<9日目 |Clang with Microsoft CodeGenがでたので試す
<<16日目 | ブログズミ: Boost.Test v3 を使ってみた](http://srz-zumix.blogspot.jp/2015/12/boosttest-v3.html) || [18日目 | それC++なら#defineじゃなくてもできるよ | とさいぬの隠し部屋 >>
次は18日目、myon___さんの「それ #define じゃなくてもできるよ」ですね。constexprとか出るのかな~~(きっと出ない)~~と思ったら触りだけでた