Katsuoのサイト
c++の基礎や便利だと思ったことなど、主に自分用としてメモしていきます。
公式の日本語ドキュメントが意外とわかりやすいのでわからない機能などを参照する。
gccの公式ドキュメントも読んでいく。
linux使っていてvimでc++プログラムを書いてコンパイルと実行する手順を忘れるのでメモ。
windowsでも同じ方法でコンパイルができる(mingw)。windowsの場合は、実行はa.exeをそのまま実行するだけ。
コンパイル:
$ g++ -c sample.cpp -o sample.o
実行ファイル作成し実行:
$ g++ sample.o //a.outという実行ファイルが生成される
$ ./a.out //実行(windowsではa.exeなどそのまま実行ファイルを実行)
g++ file.cppとシンプルにしてもコンパイルと実行ファイルa.exe(windowsの場合)が生成される。
この場合はオブジェクトファイルは生成されない。
c++ –versionやg++ –versionとしてもどのバージョン(c++11,C++14,C++17など)のc++なのかが分からない。
最新のg++をインストールしていると、おそらく最新のc++が使えるようになっていて、必要に応じて任意のバージョンでの
コンパイルができるようになっている。
コンパイルのときに、”__cpluscplus”というプリプロセッサマクロが使われている。
このマクロの値として、c++のバージョンごとに決まった値があり、
c++17は201703L,c++11は201103Lといった値を持っている。
このマクロがどの値を持っているかを確認することで、どのバージョンでコンパイルされているかを
確認できる。
もっと簡単にバージョンが分かる方法はないのかと思うので、継続テーマとして調べていく。
下記のコードを実行すると、どのバージョンでコンパイルされているかを出力できる。
//printcppversion.cpp
#include <iostream>
int main() {
if (__cplusplus == 201703L) std::cout << "C++17\n";
else if (__cplusplus == 201402L) std::cout << "C++14\n";
else if (__cplusplus == 201103L) std::cout << "C++11\n";
else if (__cplusplus == 199711L) std::cout << "C++98\n";
else std::cout << "pre-standard C++\n";
}
ファイルをコンパイルし実行するとバージョンが表示される。
g++ printcppversion.cpp
a.exe
//c++17
cplusplusマクロはc++の翻訳単位のコンパイルに使われるため、この値の有無で条件分岐して狙った動作をさせる。
g++は.cファイルも.cppファイルもc++ファイルとしてコンパイルをする。
gccは.cの時はc言語としてコンパイルし、.cppの時はc++としてコンパイルする。
g++はリンクの時にstdc++ライブラリをリンクするが、gccはリンクしない。
上記のような違いがある。
関数名などに対してコンパイラが内部的に名前を修飾する機能のこと。これによって、同じ関数名でも内部的にちゃんと異なる関数として
認識されて処理される。c++には名前修飾機能があるので同じ関数名が存在できるけど、c言語には名前修飾の機能は無いので同じ関数名が
存在できない。
wikipediaの記事が分かりやすかった。
実際の名前修飾の状態などを確認できるコマンドとして、nmコマンドというものがある。windowsでも実行できるので、
実際に試すといいかも。nm –helpで使い方が確認でき
nmコマンドにオブジェクトファイルを渡すと内容が表示される。
例)
>nm sample.o
名前修飾された状態が表示される。デマングルの状態で表示するには、nm –demangleオプションで上記コマンドを実行すると、
名前修飾されていない、見やすい状態でnmコマンドの実行結果をみることができる。
オブジェクトファイルをリンクする機能は、c用とc++用のリンケージがあり、デフォルトではc++用のリンケージが動作する。
c言語のオブジェクトファイルとc++のオブジェクトファイルをリンクするとき、上記により、c++のリンケージが動作する。
c言語で作成されc言語としてコンパイルされた関数をc++から呼び出して使うとき、リンク処理をするが、
c++用リンケージがデフォルトで使われる。c++用のリンケージには、名前修飾されたもの(関数など)が登録されている。
一方、c言語では名前修飾自体が機能としてないので、c言語としてコンパイルされた関数は、名前修飾されていない。
そのため、c++のリンケージが動作する際、c++のリンケージは名前修飾されていることが前提条件として必要だけど、
c言語の関数は名前修飾されていないのでリンクができず、c言語で作成された関数をc++側から使えないといった不具合が起こる。
上記を回避するために、extern “C”という機能を使う。
extern “C” <関数>のように、関数名の前に付与することで、c言語の関数と認識させることができ、名前修飾なしでc言語の関数として リンク処理しc++側で使うことができる。関数>
クラスを定義したときに下記メンバ関数が暗黙的(自動的)に定義される。
デフォルトコンストラクタ.
コピーコンストラクタ.
ムーブコンストラクタ.
コピー代入演算子.
ムーブ代入演算子.
デストラクタ.
何もしなくても暗黙的(自動的)に定義されるコンストラクタに対しdefault宣言またはdelete宣言することで、
暗黙的にされてしまう定義に対して挙動を変えることができるイメージ。
default宣言、delete宣言を付与する対象(上記に挙げたコンストラクタ類)についてどんなものかを理解しておく必要がある。
暗黙定義される特殊関数を制御するために “= default”と “= delete”機能が追加された。
”= default”は暗黙定義されるコンストラクタ類を使用し、inlineやvirtualなどの修飾のみを明示的に付与したいときに使用する機能。 (下記サンプルコードで定義した以外のものは暗黙定義されたものが自動的に使用される)
class A {
public:
//暗黙的に定義されるデストラクタにvirtualを付与しデフォルトで使用されるよう指定
virtual ~A() = default;
// 暗黙的に定義されるコピーコンストラクタにinlineを付与しデフォルトで使用されるよう指定
inline A(const A&) = default;
};
”= delete”は暗黙定義を明示的に禁止するための機能で、コピーを禁止するクラスを定義するような場合に使用する。
class X {
public:
// コピーは禁止するがムーブは許可する
//コピーコンストラクタおよびコピー代入演算子を禁止
X(const X&) = delete;
X& operator=(const X&) = delete;
// 特殊メンバ関数を明示的に定義またはdeleteした場合は、
// それ以外の特殊メンバ関数は明示的に定義もしくはdefault宣言しなければ暗黙定義されない(下記)
//デフォルトコンストラクタおよびムーブコンストラクタおよびムーブ代入演算子を使用する
X(X&&) = default;
X() = default;
X& operator=(X&&) = default;
};
int main()
{
X x1;
//X x2 = x1; // コンパイルエラー。Xのコピーコンストラクタ(左辺値参照)はdelete宣言されている
X x3 = X(); // OK : ムーブコンストラクト(右辺値参照)はできる
X x4;
//x4 = x1; // コンパイルエラー。Xのコピー代入演算子(左辺値参照)はdelete宣言されている
X x5;
x5 = X(); // OK : ムーブ代入(右辺値参照)はできる
}
”= delete”は特殊メンバ関数以外にも使用可能。
void f() = delete; // OK
int main()
{
//f(); // コンパイルエラー.関数f()はdelete宣言されているので使えない
}
コピーコンストラクタとは、そもそもの前提として代入の時に自動的に呼ばれるコンストラクタで、
コンストラクタの内部で代入元のオブジェクトの値を使って、代入先のオブジェクトにコピーする(仕組み・挙動の)こと。
下記のサンプルコードではmain()関数の中でコピーコンストラクタによって暗黙的にposAの値がコピーされて. posBにセット(代入)される。
コピーコンストラクタを定義していない場合は自動的に標準のコピーコンストラクタが使われる。
挙動としては、クラスのメンバ変数の値がそのままコピーされることになる。
下記サンプルの場合は、ZZZ posB = posA;によって、コピーコンストラクタのZZZ::ZZZ(int tmpx, int tmpy) が呼ばれ、posBの変数xにposAの29,posBの変数yにposAの30が代入されることになる。
#include <stdio.h>
class ZZZ
{
public:
int x;
int y;
ZZZ(int tmpx, int tmpy); // クラスZZZのコンストラクタ
};
// コピーコンストラクタ
ZZZ::ZZZ(int tmpx, int tmpy) // <基底クラス名>::<コンストラクタ名>()
{
x = tmpx;
y = tmpy;
}
int main()
{
ZZZ posA(29, 30); // posAに対するコンストラクタ呼び出しで29,30がセットされる
ZZZ posB = posA; // posBに対するコピーコンストラクタ呼び出し
printf("posB.x:%d posB.y:%d", posB.x, posB.y); // posB.x:29 posB.y:30
return 0;
}
同じクラス内でポインタの役割を与えられたオブジェクトAがあるとし、ポインタが指し示すメモリ領域をMとする。
すると、オブジェクトAのデータはメモリ領域Mに存在する。
この状態で他のオブジェクトBをコピーコンストラクタによってオブジェクトAのデータで初期化すると、
オブジェクトAとオブジェクトBは同じメモリ領域Mのデータを共有していることになる。
この状態でデストラクタでどちらかのオブジェクトを削除すると、メモリ領域Mのデータが消去されるので、
もう片方のオブジェクトのデストラクタが実行されてメモリ領域Mのデータを削除しようとした時に既に、
メモリ領域Mにデータが存在しないので例外エラーとなる。
そのほかにも、2つのオブジェクトで共有しているデータを片方のオブジェクトが変更してしまうと、
もう片方のオブジェクトにも予期しない影響が出てくる。
コピーコンストラクタ内の処理で新規でメモリ領域を作成しておくことで、メモリ領域を共有しないようにし、
オブジェクトごとにメモリ領域を確保するようにしておく。すると、コピーコンストラクタを使うときにデータはコピーにより
同じでも、格納されるメモリ領域が異なるので同じメモリ領域を共有することによる前述のような不具合を避けることができる。
// コピーコンストラクタ
Neko::Neko(const Neko & neko)
{
// ヒープメモリを新たに確保
nName = new char[strlen(neko.nName) + 1]();
// コピー元オブジェクトの猫名をコピー(コピー元のneko.nNameの値をコピー先のnNameにコピー)
strcpy_s(nName, strlen(neko.nName) + 1, neko.nName);
}
<クラス名>::<コンストラクタ名>(const <クラス名>& <オブジェクト名>){<コピー処理の内容>}
引数にconst修飾したクラスの参照型のオブジェクトを渡す。
言い換えると、参照しにいくクラスをconst修飾し、そのconst修飾したクラスをもとにオブジェクト. 生成しそのオブジェクトを引数とする。
クラスをconst修飾しておくことで、参照するクラスのメンバが勝手に書き換えられないことを保証する。
もしコピーコンストラクタが参照するクラスのメンバが勝手に他から書き変えてしまっていると、
それはコピーコンストラクタにとっては、予期せぬことかもしれないので期待と違う結果になってしまうかもしれない。
そのため、コピーコンストラクタが参照するクラスをconst修飾しておき、コピーコンストラクタで参照するメンバが.
他から勝手に書き換えられないようにしておく。
下記サンプルコードではコピーコンストラクタの定義をすることでコピーコンストラクタの挙動を明示している。
class ZZZ
{
public:
int x;
int y;
ZZZ(int tmpx, int tmpy); // int型の引数を渡した場合に実行されるコピーコンストラクタ
ZZZ(const ZZZ & pos); // ポインタオブジェクトの引数を渡した場合に実行されるコピーコンストラクタ
};
// コンストラクタの実装内容
ZZZ::ZZZ(int tmpx, int tmpy)
{
x = tmpx;
y = tmpy;
}
// コピーコンストラクタの実装内容
ZZZ::ZZZ(const ZZZ & pos)
{
x = pos.x;
y = pos.y;
}
コピーコンストラクタは次のように呼び出すことができる。
ZZZ posA(100, 200); // 引数にint型引数2個を渡した時に実行されるコピーコンストラクタが呼ばれる
ZZZ posB = posA; // 引数にオブジェクトを渡したときに実行されるコピーコンストラクタが呼ばれる
このサンプルコードでは、posBオブジェクトの初期化に”=”演算子を使うことでコピーコンストラクタを呼び出している。
他にも、下記サンプルコードのように()でコピーコンストラクタを呼び出すこともできる。
引数の種類によってどのコンストラクタを呼ぶかが判断され呼ばれる。
ZZZ posA(100, 200);
ZZZ posB(posA); // ()を使ったコピーコンストラクタの呼び出し
右辺値. 名前をもたない一時的なオブジェクト
左辺値. 明示的に実態のある名前付きオブジェクト。
右辺値参照とは C++11で追加された機能。
右辺値は基本的に評価された時点でデストラクタによって消されてしまう。 なので右辺値を生かしておくには右辺値への参照を持つ右辺値参照型という型で束縛する必要がある。
右辺値参照は、<参照するものの型>に<&&>を付加して宣言することで右辺値を参照する。 例えば"int&&"はint型の右辺値を参照する右辺値参照の型名を宣言している。参照するものの型>
右辺値参照型は右辺値への参照を持つための型のため左辺値。 右辺値参照型は左辺値なので、すぐ消されてしまう右辺値を保持することが可能になる。
int num = 100; // num は左辺値
int&& ref1 = 100; // OK。100 は右辺値。右辺値を参照している
int&& ref2 = num; // コンパイルエラー。numは左辺値なので参照できない
int&& ref3 = ref1; // コンパイルエラー。左辺値であるref1を参照しようとしている
ムーブコンストラクタはオブジェクトの内部値を新たなオブジェクトに移動または譲渡するための特殊なコンストラクタ。
コピーコンストラクタと同様に、オブジェクトを初期化するときにムーブコンストラクタが呼び出される。
コピーコンストラクタが値のコピーをするのに対し、ムーブコンストラクタは値自体を移動する。
コピーコンストラクタでのコピー処理だと処理の負荷が大きい時にムーブコンストラクタを定義してコピーではなくムーブをさせる.
ことで処理の効率化を図るために使う。
<クラス名>(<クラス名>&& value);
ムーブコンストラクタは元オブジェクトのコピーではなく移動をするため、
元オブジェクトのメンバは移動した後にゼロ化(元オブジェクトのメンバにnullptrを代入)する必要がある。
ムーブの場合、ムーブ元になった右辺値は通常はムーブ直後にデストラクタが呼ばれてムーブ元になった右辺値は解放されてメモリ領域自体存在しなくなる。
すると結果的に、ムーブ先の方が持っているポインタ変数が指し示す先には、もう何もないという状態になりエラーとなる(たぶん)。
この問題を解決するために、ムーブ時にムーブ元が持っているメモリ領域にnullptrを代入する。
これによって、ムーブ先の方が持っているポインタ変数が指し示す先のメモリ領域にはnullptrが入っているのでデストラクタが呼ばれても何も起こらない。
ムーブ直後に呼び出される一時オブジェクトに対するデストラクタでは何も起こらなくなり、正しくムーブ先に受け継ぐことができる。
ムーブコンストラクタの仮引数は自身のクラス型の右辺値参照。constは付けない。
ムーブ元から情報を移動させるためムーブ元には情報が残らないと考えられるので、constはつけなくても良い。
ムーブコンストラクタのサンプルコードは下記のようになる。
MyClass(MyClass&& value)
{
m_description = value->m_description;
value->m_description = nullptr;
}
まだ勉強するには早いかもしれないけどスマートポインタという機能をメモ。
C++11から追加された機能で、newしたあとにdeleteを忘れてメモリ領域が解放されないままとなってしまう
ことを防止できる機能。詳細は下記リンクで勉強する。
多重継承で異なる2つ以上の親から同じメンバ名を継承しているときに、どの親のメンバなのか判断ができなくなる問題を解決
できるのが仮想継承。
仮想継承の例:
struct Base {
virtual ~Base() {}
virtual void method();
};
struct Derived1 : virtual Base {}; //Derived1はBaseから仮想継承
struct Derived2 : virtual Base {}; //Derived2はBaseから仮想継承
struct Derived3 : Derived1, Derived2 {}; //多重継承
Derived3 obj;
obj.method(); //仮想継承によりBase::method();が呼ばれる。
関数オブジェクトとは、関数のようにふるまうことができるオブジェクトのこと。
クラスに関数呼び出しのための演算子を付与することで関数オブジェクトにできることが多い。
オブジェクトを呼び出すと、オブジェクトに定義している関数が呼び出される。
c++ではoperator() という演算子を付与する。
関数オブジェクトは変数に格納したり、引数に渡したり、テンプレートやクラスのメンバとして
渡したりと、柔軟性があるので便利。
標準ライブラリで関数オブジェクトが多く使われている。
//構造体Sを定義
struct S {
int value1_;
int value2_;
};
//構造体Sを型として配列sを定義
vector<S> s;
//Sのvalue1_を比較するクラス
//<演算子でvalue1_の値の小さい順にソート
struct comp1 {
bool operator()(const S& l, const S& r) const {
return l.value1_ < r.value1_;
}
};
//Sのvalue2_を比較するクラス
//<演算子でvalue1_の値の小さい順にソート
struct comp2 {
bool operator()(const S& l, const S& r) const {
return l.value2_ < r.value2_;
}
};
comp1 c1; //c1は関数オブジェクト
comp2 c2; //c2も関数オブジェクト
sort(s.begin(), s.end, c1); //value1_の値でソート
sort(s.begin(), s.end, c2); //value2_の値でソート
value1_,value2_はint型なので、比較演算子”<”により、小さい値が左、大きい値が右となり、
結果として小さい順に並ぶ。
bool operator()…のところはbool型を返す。
正常に処理してtrue、正常に処理できない場合はfalseを返す。
もし比較演算子の左側と右側が同じ数値だと、return l.value2_ < r.value2_;と処理できないので
falseが返され、ソートできない。
テンプレートで作成するクラスや関数の実引数の数が決まっていない場合、可変長引数テンプレートの書式を使うことで、
可変長の実引数にも対応できる。
//クラステンプレートの書式
template <class... テンプレート仮引数パック名> [ struct | class ] クラス名;
//関数テンプレートの書式
template <class... テンプレート仮引数パック名>
戻り値の型 関数名(テンプレート仮引数パック名... 関数仮引数パック名);
“…“は、仮引数の前に置くことで、複数の仮引数がまとめられ、仮引数の後ろに置くことで
複数の仮引数を展開する機能がある。
“テンプレート仮引数パック”には型名(typename)が入る。
“関数仮引数パック”には型名と1セットになる関数の仮引数が入る。
//...Argsはテンプレート仮引数パック。typename(型名)が複数入る
template <typename ...Args>
//...argsは関数仮引数パック。関数に渡す引数が複数入る
void f(Args ...args);
Argsには型名、argsには引数が入る。例えば、argsに{1, 2, 3.0}が入っているとすると、
Argsにはargsである{1, 2, 3.0}に対応する型名である{int, int, double}が入っている。
“テンプレート仮引数パック”および、”関数仮引数パック”は、”…“を前に置くことで、
それぞれ複数の引数(パラメータ)がまとめられた状態となっていて、これをパラメータパックとよぶ。
パラメータパックにまとまっている引数(パラメータを)展開するときは、”…“をパラメータパックの後ろに置く。
“テンプレート仮引数パック”および、”関数仮引数パック”の後ろに”…“を書くことで、ほかの関数やクラスに中身を展開して
渡すことができる。
classの後ろに…とひっついてセットのように見えるけど、”class”と”…“は関係ないので勘違いに注意。
詳しくはドキュメントがわかりやすい。
qiitaのわかりやすい記事も。
intやcharなど型に対して別名をつけることが可能で、テンプレートに対しても別名をつけることが可能で、
それをテンプレートに対する別名という意味合いでエイリアステンプレートと呼ぶ。
エイリアステンプレートを利用することで、長い記述を自分で短い名称にできるので、可視性がよくなる。
型に別名をつける方法として、typedefとusingがある。usingを使用した型の別名定義のときだけ、
テンプレートを使用できる。typedefを使った型の別名定義をしている場合は、テンプレートに使用できない。
エイリアステンプレートは特殊化ができない(おそらくエイリアスのため勝手にエイリアスを特殊化できないにようになっている?)。
typedef:
例えば、intという型名をSeisuuという型名にしたいとときは、typedef int Seisuu;と書く。
intをSeisuuという型名にするという意味。
これでSeisuu A = 3;のように、Seisuuを型名として使える。
このように、特定の役割をもった変数などの意味をわかりやすくするために、型名を自分で名付けることができる。
using:
typedefよりも別名をつける際の可読性をよくできるのが、using宣言。
usingでもtypedefのように型に別名をつけることができる。
usingを使った型の別名定義の例:
using NekoCount = int;
int型をNekoCountという別名にしている。typedefよりも見た目上、分かりやすい。
テンプレートでは、仮引数の具体的な型(実引数)に対応して関数またはクラスをインスタンス化(生成)する。
この動作を特殊化という。
テンプレートがインスタンス化されるときの具体的な型の種類で、処理を条件分岐のように分けたい
場合に利用するのが、完全特殊化や部分特殊化。
普通のテンプレートをプライマリーテンプレートと呼ぶ(完全特殊化及び部分特殊化のテンプレートと分ける意味合い)。
完全特殊化は、テンプレート仮引数に特定の型が当てはめられたときにだけ使われる
特別なテンプレートを定義する機能。
完全特殊化はクラステンプレートと関数テンプレートのどちらでも機能させることができる。
部分特殊化はクラステンプレートに対して使える機能で、関数テンプレートに対しては使えない。
関数テンプレートの完全特殊化の例:
//通常の関数テンプレート
template <typename T>
T DoSomething(T a, T b) {
return a + b;
}
//完全特殊化(実引数に小数が渡されると下記の関数テンプレートで特殊化が実行される)
template <>
double DoSomething<double>(double a, double b) {
return a * b;
}
std::cout << DoSomething(3, 4) << std::endl; // 7
std::cout << DoSomething(3.0, 4.0) << std::endl; // 12
部分特殊化は、クラステンプレートの一部のテンプレート引数を確定させたり、制限したりすること。
完全特殊化のように、対応する型を完全に決めるのではなく、ある範囲の型に対して動作を定義すること。
テンプレートによって生成された型ごとの関数や型はテンプレートのインスタンスとよぶ。
インスタンス化は翻訳単位でされるので、同じ引数で複数のユーザーからインスタンスを作ると、
各ユーザーがそれぞれインスタンスを作るので、同じインスタンスがいくつも作られてしまい、
オブジェクトファイルのサイズが膨れ上がるなど、非効率的になる。
extern template と宣言すると、そのテンプレートでのインスタンスは外部参照になるので、同じインスタンスが作られることを防止できる。
分かりやすいコードは公式リファレンス参照。
コンパイル時に計算されるものと、実行時に計算されるものがあり、c++11以降、コンパイル時計算をする方向になっている。
ソースコード中に書いたリテラルだけを使った計算は、コンパイル時に計算される。
コンパイル時に計算を済ませておくことで、プログラム実行が高速になる。
リテラルだけを使うことで、コンパイル時に計算できるようにしている式を定数式と呼ぶ。
“constexpr”で定数式にでき、変数と関数で使える。
constexpr 変数:
定数式扱いとなり、コンパイル時に計算される。
constexpr 関数:
定数式扱いとなり、コンパイル時に関数が実行される。
関数がコンパイル時に実行されるためには、関数で扱う変数も定数式じゃないといけない。
定数式の定義例
constexpr int v = 100;
ラムダ式の記法を使うと、簡単に関数オブジェクトを定義できる。ラムダ式の計算結果を他の関数の引数に渡して使ったりする。
ラムダ式の説明はリファレンスが分かりやすい。
プリプロセッサはコンパイルの前に前処理を行う機能。
前処理とは、ファイルの読み込み、マクロの処理、条件付き取り込みの範囲設定など。
ファイルの読み込み:
#include命令や__has_include命令。
“__has_include”命令を使うと、ソースファイルが存在したら読み込むという動作が可能になる。
例)
#if __has_include(<fileName>)
#include <fileName>
#endif
マクロの定義:
#define/#undef。マクロはプリプロセッサによって文字列を置換する。
マクロにはオブジェクト形式マクロと関数形式マクロがあり、どっちもマクロが使用された箇所で文字列の置換のみをする。
オブジェクト形式マクロの使用例)
//オブジェクト形式マクロの定義
#define DEFAULT_COFFEE_VOLUME 150
...
const int defaultCoffeeVolume = DEFAULT_COFFEE_VOLUME;
//プリプロセッサによって置換される↓
//const int defaultCoffeeVolume = 150;
ソースコードの中でDEFAULT_COFFEE_VOLUMEという文字列を使うとプリプロセッサが150で置換してくれる。
関数形式マクロの使用例)
//関数形式マクロの定義
#define ADD(VAL) VAL + VAL
...
//マクロの使用
const int result = ADD(1);
//プリプロセッサによって置換される↓
//const int result = 1 + 1;
...
//マクロの削除
//削除を明示しない場合はソースファイルの終端までマクロが有効
#undef ADD
関数マクロの仮引数に#を付加すると、マクロに渡された実引数を文字列にできる。
#define TO_STRING(a) #a
//上記マクロの呼び出し(実引数として文字列"hello"を渡している)
const char* str = TO_STRING(hello);
//下記のように展開される
//const char* str = "string"
関数マクロの仮引数は##によって連結できる。
#define CONCATENATE(a, b) a##b
//関数マクロに実引数を渡して呼び出し
CONCATENATE(Hello, World)
//下記の結果が得られる
//HelloWorld
coutは標準出力、cerrは標準エラー出力で使う。
変数宣言でthread_localキーワードを指定することで、スレッドごとの静的記憶域に変数が保持される。
staticを使用した変数はプログラムを通してひとつの状態を持ち、プログラム終了時に変数が破棄される。
thread_localを使用した場合はスレッドごとに状態を持ち、スレッド終了時に変数が破棄される。
エラーハンドリングとは、プログラム中で何かの問題によって発生したエラーに対処する処理のこと。
エラーハンドリングをするためには、エラーが発生したときにそのエラーを対処する部分までエラーの
情報を伝える必要がある。
戻り値によるエラーハンドリング、例外によるエラーハンドリング、スレッドローカル変数によるエラーハンドリング。
それぞれの方法に長所もあれば短所もある。
戻り値によるエラーハンドリング:
一般的によく使用されているエラーハンドリングだが、正常な処理にエラー処理のコードが挟まれることになるので、
ソースコードが煩雑になりやすく、コードの見通しが悪くなることで、戻り値の確認漏れなどでバグの元になったりする。
例外によるエラーハンドリング:
例外によるエラーハンドリングは、例外の送出にthrow、例外の補足にcatchを使用する。
例外を使用したエラーハンドリングは、例外が発生するとその時点で関数の処理が中断されてしまうので、
関数の処理が不完全になって、メモリリークが発生したりする。
また、throw,catchを使うと処理の実行コストが高くなるので、パフォーマンスが悪くなりやすい。
戻り値を使ったエラーハンドリングよりもソースコード作成の難易度が高い。といった特徴がある。
スレッドローカル変数によるエラーハンドリング:
エラーが発生したときに、エラーを表す値を変数や関数にセットして、呼び出し元に処理が戻った時点で値を確認して
エラーが発生しているかどうかを判定する。
マルチスレッドプログラムでは、複数のスレッドからエラー値が上書きされてしまうので、エラー情報が正しく伝えられない。
そのため、マルチスレッドプログラムではスレッドローカル変数を使用してエラーハンドリングをする。
optionalクラスは任意の型T(int,stringなど)の値を有効値としていろんな型に共通の無効値状態を表現できる型。
namespace std {
template <class T>
class optional;
}
optionalクラスには大きく2つの用途がある:
普通はオブジェクトは定義した時点で初期化される。optionalクラスはこの初期化タイミングを遅延させるために使用できる。
初期化タイミングを遅延させる用途にstd::shared_ptrのようなスマートポインタを使用することもできるけど、
optionalクラスは動的メモリ確保を行わないため、リソース管理じゃなく初期化タイミングを遅延させるだけなら
optionalクラスを使う方が適している。
エラー報告について、opyionalクラスを使用しない場合、従来は有効値と無効値は以下のように表現されていた。
上記のように、有効値と無効値の表現は、変数単位またはAPI、ライブラリ単位での仕様。これに対し
optionalクラスでは、nulloptという特殊な定数を無効値とし、様々な型に共通の無効状態を持たせられるようになっている。
optionalクラスはヒープ領域を使って動的メモリを確保しない。
配置newやstd::aligned_storageのような機能によってスタック領域のメモリのみを使う。
optionalを使ったsample:
#include <iostream>
#include <optional>
// 除算をする関数。
// ゼロ割りを試みた場合無効値が返る
std::optional<int> safe_divide(int a, int b)
{
if (b == 0)
return std::nullopt;
return a / b;
}
int main()
{
// 10/2を計算する
std::optional<int> result1 = safe_divide(10, 2);
if (result1) { // 計算に成功した場合、有効値が返る
int x = result1.value(); // 有効値を取り出す
std::cout << x << std::endl;
}
// 5/0の計算を試みる
std::optional<int> result2 = safe_divide(5, 0);
if (!result2) { // 計算に失敗した場合、無効値が返る
std::cout << "error" << std::endl;
}
}
//出力
5
error
c++における文字列型は3種類ある。
stringクラスを利用した文字列オブジェクトの例。
#include <iostream>
#include <string>
using namespace std;
int main(){
string a;
string b = "sample";
string c(b, 1, 3);//bの文字列の1文字目から3文字までの文字列オブジェクトを作成する
string d("sample", 2);//文字配列の先頭2文字を文字列オブジェクトとして作成する
string e(10, 't');//10文字分のtで文字列オブジェクトを作成する
string f = {'s', 'a', 'm', 'p', 'l', 'e'};//文字の初期化子リストから文字列オブジェクトを作成する
string g = b;//bの文字列オブジェクトをgにコピーすることで文字列オブジェクトを作成する
string h0 = b;
string h = move(h0);//h0の文字列オブジェクトを左辺値のhにmove(hの*thisに移動)することで文字列オブジェクトを作成する
string i(b.begin(), b.end());//イテレータで指定された範囲から文字列オブジェクトを作成する
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
cout << "d:" << d << endl;
cout << "e:" << e << endl;
cout << "f:" << f << endl;
cout << "g:" << g << endl;
cout << "h:" << h << endl;
cout << "i:" << i << endl;
}
//出力結果
a:
b:sample
c:amp
d:sa
e:tttttttttt
f:sample
g:sample
h:sample
i:sample
bool値を文字列として入出力することを指示するマニピュレータ。
streamの中でbool値の演算をすると結果をbool値として出力してくれる。
マニピュレータとは、C++のストリームの形式を変えるための関数などのこと。
マニピュレータはヘッダーファイルとして
boolalphaは
#include <iostream>
#include <string>
using namespace std;
int main() {
string a = "123";
string b = "123";
cout << boolalpha;
cout << "== : " << (a == b) << endl;
cout << "!= : " << (a != b) << endl;
cout << "< : " << (a < b) << endl;
cout << "<= : " << (a <= b) << endl;
cout << "> : " << (a > b) << endl;
cout << ">= : " << (a >= b) << endl;
}
//出力結果
== : true
!= : false
< : false
<= : true
> : false
>= : true
上記のように、std::basic_strngクラスでは演算子が文字列に対して使える。
stringクラスを利用する。
begin()/end()メンバ関数は先頭要素を指すイテレータ / 末尾要素を指すイテレータを返す。
rbegin()/rend()メンバ関数は逆順のイテレータを返す。
例えば”xyz”という文字列オブジェクトの場合、rbegin()は’z’を指すイテレータを返し、
rend()は’x’の前を指すイテレータを返す。
cbegin()/cend()メンバ関数は、文字列オブジェクトがconstに指定しているかしていないかにかかわらず
常にconst_iteratorを返す。もし、文字列オブジェクトの内容が変わると、イテレータが無効になる。
#include <iostream>
#include <string>
using namespace std;
void foo(string::const_iterator a) {
//何もしない(でもあとでcbegin()イテレーターで使う)
}
void foo(string::iterator a) {
*a = 'x';
}
int main(){
//範囲for文で文字列を走査する
string x = "hello";
cout << "x:";
for (const char& c : x) {
cout << c;
}
cout << endl;
//逆順イテレータの範囲で初期化
string y(x.rbegin(), x.rend());
cout << "y:" << y << endl;
//cbegin()/cend()はconst_iteratorを返す
foo(y.cbegin());
cout << "y:" << y << endl;
//begin()/end()はオブジェクトの状態によってiteratorかconst_iteratorかが切り替わる
foo(y.begin());
cout << "y:" << y << endl;
}
//実行結果
hello
olleh
olleh
xlleh
最後の出力結果がxllehとなる理由:
begin()が対象とする文字列オブジェクトがollehであり、これはconstな文字列オブジェクトじゃない。
なのでbegin()はconst_iteratorじゃなく、普通のiteratorとなる。なので、下記の関数が呼ばれる。
ここで、foo(y.begin())のy.begin()はollehのoを指すイテレーター。このoが下記関数に渡されることになる。
void foo(string::iterator a) {
*a = 'x';
}
この関数では、引数に渡されてきたイテレータの文字の値を’x’に書き換える。そのため、引数として渡したy.begin()イテレーター
の値であるoがxに書き換わる。そのため、文字列オブジェクトyの値はxllehになる。