C++言語 補足
C++(GCCのg++で動作確認)の補足説明です。
ANSI C++で追加されたキーワードと、C++言語を学ぶで説明不足だった部分などを解説しています。
解説一覧
(1)変数の宣言の仕方
(2)引数名省略
(3)constメンバ関数
(4)noexcept
(5)decltype
(6)キャスト
(7)explicitと型変換コンストラクタ
(8)mutable
(9)typeid
(10)namespace
(11)using
(12)可変引数テンプレート
(13)関数オブジェクト
(14)ラムダ式
(15)alignas
(16)alignof
(1)変数の宣言の仕方
下記の①、②ようにC言語風に変数を宣言(初期化)できるのはもちろんですが、
C++では③、④のように変数を宣言(初期化)することもできます。
① int x = 0;
② int y = {0};
③ int x(0);
④ int y{0};
①②はコピー初期化と呼ばれています。
③④は直接初期化と呼ばれています。
コピー初期化はコピーする際に型変換の余計な処理が含まれるそうですが、直接初期化は余計な処理が含まれません
(少しでも高速なコードを記述したい場合には直接初期化の方の記述をした方が良いかもしれませんが、
個人的にはC言語風の書き方の方が分かりやすくて良いと思います。
いや、直接初期化の方に慣れるべきかもしれません)。
(2)引数名省略
C++では関数定義のとき、使用しない引数は引数名を省略することができます。
ただし、呼び出し側は必要な数だけ引数を渡す必要があります。
#include <iostream>
using namespace std;
// 第2引数は使用しないので名前を省略
void func( int a, double )
{
cout << a << "\n";
}
int main()
{
// 呼び出し側は省略不可
func( 11, 3.14 );
}
(3)constメンバ関数
下のプログラムのfunc関数の横にある「const」の意味についてです。
class A
{
public:
void func( void ) const // ←このconstです
{
}
};
関数の横にconstが付いている場合、そのメンバ関数内ではメンバ変数の変更ができなくなります。 また、メンバ変数を変更するような関数の呼び出しを行うとコンパイルエラーになります。
(4)noexcept
noexceptは、関数がどの例外を送出する可能性があるかを列挙するのではなく、
例外を送出する可能性があるかないかのみを指定するもので、関数定義直前({ の直前)あたりに記述します。
例外を送出する可能性がある関数にはnoexcept(false)を指定し、
例外を送出する可能性がない関数にはnoexcept(true)もしくはnoexceptを指定します。
noexceptキーワードのもうひとつの意味は、
式が例外を送出する可能性があるかどうかを判定する演算子です。
noexcept(f(arg))のようにnoexcept演算子に式を指定することで、
その式が例外を送出する可能性があるかどうかを、コンパイル時定数のbool値として取得できます。
つまり、関数に対して指定されたnoexceptの情報を取得します。
以下に例を示します。
class myclass {
int value = 0;
public:
int getValue() const noexcept(false) { // noexcept(true)にするとコンパイルは通ります
return value;
}
};
myclass x;
// noexceptはtrueが返ってくるのでstatic_assertによりコンパイルエラーとなります。
static_assert(noexcept(x.getValue()), "getValue() is noexcept(false)");
(5)decltype
decltypeは続く式の型を取得する特殊な機能です。
int a = 3; int b = 2; int &r = b; decltype(a + b) c = a + b; // cの型はint decltype(auto) d = a + b; // dの型もint。autoが式「a + b」で置き換えられる decltype(r) x = r; // xの型はint&
(6)キャスト
C言語の(旧式の)キャストはC形式と関数形式の2つの形式で記述することができます。
double d = 2.5; int i = (int)d; // C形式 int j = int(d); // 関数形式
C言語ではキャスト(型変換)は結構自由に出来てしまっていましたが、
バグを作らないようにするためでしょう、C++ではもう少し厳密なキャストの仕方になった模様です。
C++で追加されたキャストに使う演算子は以下の4つです。
- const_cast
- static_cast
- dynamic_cast
- reinterpret_cast
これらのキャスト演算子の使い方は、
キャスト演算子 < 型名 > ( 式 )
となります。
①const_cast
const_castはconstやvolatileを無効化するために使用します。
const int *cp = nullptr; int *p = const_cast(cp);
nullptrはヌルポインタを表すキーワードで、C++11からはnullptrキーワードでヌルポインタ値を表すことを推奨しているようです。
②static_cast
static_castは一般的なキャストを行います。 charからshort、shortからlong、列挙型からint型などのキャストを行います。 ポインタ型のキャストは行えません。ポインタ型のキャストはreinterpret_castを用います。
enum class em {
EM1 = 5,
EM2,
EM3
};
int main()
{
char c;
short s;
long l;
double d;
em e;
c = static_cast(em::EM3);
s = static_cast(c);
l = static_cast(s);
d = static_cast(l);
d = 21.5;
l = static_cast(d);
s = static_cast(l);
c = static_cast(s);
}
③dynamic_cast
dynamic_castは、実行時にオブジェクトのポインタのキャストを行います。
子クラスのポインタを親クラスのポインタに型変換することをアップキャストと言います。
逆にダウンキャストとは親クラスのポインタを子クラスに型変換することを言います。
アップキャストはdynamic_castを用いなくても、子クラスは親クラスの情報を持っていますのでキャスト可能です。
逆にダウンキャストは親クラスの情報しか持っていない(親クラスのオブジェクトを指し示している)可能性があり、
その場合は(dynamic_castを使って)キャストできませんので失敗します。
オブジェクトのキャストが必要なときとは仮想関数(実行時ポリモーフィズム)を利用するときでしょう。
dynamic_castは実行時に指し示しているポインタのオブジェクトをチェックして、
キャスト可能であればそのアドレスを、キャスト不可のときはNULLを返します。
ここで「キャスト可能」とは、指し示しているアドレスのオブジェクトがキャストしようとする型と一致している...
ということです。
無理やり異なる型のオブジェクトのポインタをdynamic_castを使ってキャストすることはできないということです。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func(){};
};
class Derived : public Base
{
};
int main()
{
Base *bp1 = new Derived();
Derived *dp1 = dynamic_cast (bp1); // 成功します
if( dp1 )
cout << "dp1 dynamic_cast 成功" << endl;
else
cout << "dp1 dynamic_cast 失敗" << endl;
Base *bp2 = new Base();
Derived *dp2 = dynamic_cast (bp2); // 失敗します
if( dp2 )
cout << "dp2 dynamic_cast 成功" << endl;
else
cout << "dp2 dynamic_cast 失敗" << endl;
Base *bp3 = dp2; // これはエラーになりません
delete bp1;
delete bp2;
}
④reinterpret_cast
reinterpret_castは、互換性のないポインタ型同士の変換や、整数型とポインタ型との変換などに用います。
例えば③dynamic_castのサンプルプログラムで失敗したキャストも、このreinterpret_castを用いればキャストできてしまいます。
...しかし、それ自体には意味がありませんのでご注意ください。
あまり参考にならないかもしれませんが、
③dynamic_castで失敗したキャストをreinterpret_castを使ってキャストしたサンプルプログラムです。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func(){};
};
class Derived : public Base
{
};
int main()
{
Base *bp2 = new Base();
Derived *dp2 = reinterpret_cast <Derived *>(bp2); // これは常に成功します
if( dp2 )
cout << "dp2 reinterpret_cast 成功" << endl;
else
cout << "dp2 reinterpret_cast 失敗" << endl;
delete bp2;
}
(7)explicitと型変換コンストラクタ
「型変換コンストラクタ」とは、実引数を1つだけ指定して呼び出すことができるコンストラクタです。
また引数が2つ以上あるコンストラクタでも、2つ目以降の引数がデフォルト引数のときは「型変換コンストラクタ」と呼びます。
「型変換コンストラクタ」は暗黙の型変換を行って、
本来であればコンパイルエラーになって欲しい記述でもコンパイルが通ってしまうことがあります。
そんなときに暗黙の型変換を抑制してコンパイルエラーにするためのキーワードがexplicitです。
class A {
public:
explicit A(int){} // explicitを取ると以下のコードはコンパイル通ってしまいます。
};
void func(A f)
{
}
int main()
{
A o1 = 1; // explicitを付けるとコンパイルエラー
A o2(1); // OK
func(1); // explicitを付けるとコンパイルエラー
func(static_cast(1)); // OK
func(A(1)); // OK
}
(8)mutable
mutableはconst属性をオーバーライド(書き換え可能に)するキーワードです。
使い方の例を下記に示します。
#include <iostream>
using namespace std;
class A {
public:
A() : price(100) {};
int HowMuch(void) const { // constを指定してメンバ変数書き換え不可に
return price++; // priceはmutable指定しているので書き換え可能に
};
private:
mutable int price;
};
int main()
{
A obj;
cout << obj.HowMuch() << "\n"; // 100
cout << obj.HowMuch() << "\n"; // 101
cout << obj.HowMuch() << "\n"; // 102
}
(9)typeid
typeidは型情報を取得できます。
サンプルプログラムを下記に示します。
#include <iostream>
using namespace std;
class A{
public:
A(){};
};
class B{
public:
B(){}
};
int main(void){
char c = 10;
short s = 10;
int i = 10;
long l = 10;
float f = 10.F;
double d = 10.;
cout << typeid(c).name() << endl;
cout << typeid(s).name() << endl;
cout << typeid(i).name() << endl;
cout << typeid(l).name() << endl;
cout << typeid(f).name() << endl;
cout << typeid(d).name() << endl;
A a1;
A a2;
B b;
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(b).name() << endl;
if(typeid(a1) == typeid(a2)){
cout << "クラスが一致しています" << endl;
}else{
cout << "クラスが一致していません" << endl;
}
if(typeid(a1) == typeid(b)){
cout << "クラスが一致しています" << endl;
}else{
cout << "クラスが一致していません" << endl;
}
}
実行結果は以下のようになりました。
c s i l f d 1A 1A 1B クラスが一致しています クラスが一致していません
(10)namespace
namespaceは名前付きスコープを作成します。
以下にサンプルプログラムを示します。
namespace NS1 {
int x;
}
namespace NS2 {
int y;
namespace NS3 {
int z;
}
}
int main()
{
NS1::x = 10;
NS2::y = 20;
NS2::NS3::z = 30;
}
上記のような感じで使います。
変数x、y、zはグローバル変数なのですが名前の付いた空間に置かれるので、
NS1、NS2、NS2::NS3というスコープ名を指定しないとアクセスできません。
namespaceは(NS3のように)入れ子にすることができます。
C言語を主に使ってきた私としては「面倒だな」という印象がありますが、
沢山あるオブジェクトと変数を区別するには致し方ないのでしょう。
良く使う変数や関数をいちいちスコープ名から記述するのは面倒です。
そんなわけでusingキーワードを使用して、下記のように省略して記述できます。
namespace NS1 {
int x;
}
namespace NS2 {
int y;
namespace NS3 {
int z;
}
}
int main()
{
using namespace NS1;
using NS2::y;
using namespace NS2::NS3;
x = 10;
y = 20;
NS3::z = 30;
}
using namespace ***は***のネームスペースに所属する変数や関数をすべて省略形で使えるようにします。
using ***::+++は指定された***::+++(変数名または関数名)のみ省略形で使えるようにします。
変数名や関数名が衝突してしまう可能性がありますので、
using namespace ***形式でまとめて省略形にするのはあまり使用しない方が良いでしょう。
(11)using
usingにはいくつか使い方があります。
- (8)namespaceの項でもご紹介したように名前付きスコープを省略する使い方。
- 1と似ているのですが、クラス名を省略する使い方(下記サンプルプログラム)。
- typedefと似たような機能ですが、型を別名の型として定義する使い方。
typedefと異なるのは書き方も若干異なるのですが、templateが使えるという事です。
#include <iostream>
using namespace std;
class A {
public:
void c(char) {
cout << "In A::c()\n";
}
void d(char) {
cout << "In A::d()\n";
}
};
class B : A {
public:
using A::c; // A::c(char)が可視化され省略形c(char)で呼び出せます
using A::d; // A::d(char)が可視化され省略形d(char)で呼び出せます
void c(int) {
cout << "In B::c()\n";
c('c'); // A::c(char)が呼び出されます
}
void d(int) {
cout << "In B::d()\n";
d('c'); // A::d(char)が呼び出されます
}
};
int main() {
B obj;
obj.c(1);
obj.d('a');
}
実行結果は下記のようになります。
In B::c() In A::c() In A::d()
usingでA::cとA::dを可視化しているため、再起呼び出しではなくchar型の引数のA::cとA::dを期待通り呼び出しています。
usingの行をコメント等にして実行すると、このプログラムは再起呼び出しの繰り返しとなりますのでご注意ください。
別名の型を定義する場合、下記のような感じで記述します。
// 通常の型の場合 typedef int INT; // typedefを用いた場合 using INT = int; // usingを用いた場合 INT i; // 使い方 // 関数へのポインタの場合 typedef double (*PFUNC)(int, double); // typedefを用いた場合 using PFUNC = double (*)(int, double); // usingを用いた場合 PFUNC p; // 使い方
下記に関数へのポインタのサンプルプログラムを示します。
#include <iostream>
using namespace std;
using INT = int;
using PFUNC = double (*)(int, double);
double sub( int, double )
{
return( 2.5 );
}
int main()
{
PFUNC p; // 関数へのポインタ p
p = sub;
cout << p(1, 2.0) << "\n";
}
下記のサンプルプログラムはtemplateを使った例です。
#include <iostream>
using namespace std;
template <class X, class Y>
using PFUNC = Y (*)(X, Y);
template <class X, class Y>
Y sub( X a, Y b )
{
return( (Y)(a+b) );
}
int main()
{
PFUNC <short,int> p1; // 関数へのポインタ p1
PFUNC <float,double> p2; // 関数へのポインタ p2
p1 = sub;
p2 = sub;
cout << p1(1, 2) << "\n";
cout << p2(1, 2) << "\n";
cout << p1(2, 3.1) << "\n";
cout << p2(2, 3.1) << "\n";
cout << p1(3.1, 4.1) << "\n";
cout << p2(3.1, 4.1) << "\n";
}
実行結果は下記のようになります。
3 3 5 5.1 7 7.2
あまり良いサンプルプログラムではありませんが、なんとなくでもtemplateの使い方をご理解頂ければ良いかと思っています。
(12)可変引数テンプレート
可変引数テンプレートは、引数の数が不定のとき、省略記号(...)を使って処理します。
...が引数の前に付くときは可変引数をパックし、...が引数の後に付くときはアンパックします。
下記にサンプルプログラムを示します。
#include <iostream>
using namespace std;
// 引数が無くなった時の処理
void print()
{
cout << endl;
}
// 引数が1個になった時の処理
// (この関数は無くても動作しますが、無くすと余計なカンマが表示されます)
template <typename T>
void print(const T& t)
{
cout << t << endl;
}
// 再起呼び出しで引数をすべて処理するまで繰り返し
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) // ...が引数restの前なのでパックします
{
// 先頭の引数を表示
cout << first << ", ";
// 引数が無くなるまで再起呼び出し
// (rest...は2つ目以降の引数を表していることに注意!)
print(rest...); // 引数restの後に...が付いているのでアンパックします
}
int main()
{
int x(1);
print(); // 改行
print(x);
print(10, 20.5);
print(100.5, 200.5, 300.5);
print("一", 2, "三", 4);
}
実行結果は下記のようになります。
1 10, 20.5 100.5, 200.5, 300.5 一, 2, 三, 4
プログラムのポイントとしては、print関数を呼び出すごとに先頭の引数1つを処理するようにして、
2つ目以降の引数を再起呼び出しでprint関数に渡し、すべて処理するまで繰り返し行うというところでしょう。
アンパックは下記のところでできます。
①関数の引数
f(args...);
②テンプレートの引数
std::tuple<Args...> t;
③初期化子
int ar[] = { args... };
struct Person {
int id;
std::string name;
int age;
};
Person person = { args... };
④継承時の親(基底)クラスリストの指定
template <class... Bases>
class Derived : Bases...;
⑤コンストラクタのメンバ初期化子
template <class... Bases>
class Derived : Bases... {
Derived(Bases... bases)
: Bases(bases)... {}
};
⑥動的例外仕様(C++11から非推奨、C++17ではコンパイルエラー)
template <class... ExceptionList>
void f() throw(ExceptionList...);
(13)関数オブジェクト
関数オブジェクトは、オブジェクトを関数に見立てているだけなので、色々な情報を持つことができます。
関数オブジェクトを使えると便利なことがあるようです。
下記のサンプルプログラムはオブジェクトを作成するときに何倍するかの値を渡しておき、
関数オブジェクトの引数で渡した数値を指定倍数(サンプルでは2倍と3倍)した値を返すものです。
次の項のラムダ式を用いますと、
オブジェクトをわざわざ定義しなくても簡単に関数オブジェクトを作成できるようになります。
#include <iostream>
using namespace std;
// 関数オブジェクト
class mul {
int ml;
public:
mul( int m ): ml(m) {} // 宣言(定義)時の引数の値をmlに保存
int operator()( int x ) { // ()演算子をオーバーロードして関数オブジェクトを実現します
return( x * ml );
}
};
int main()
{
mul nibai( 2 ); // 2倍するオブジェクト
mul sanbai( 3 ); // 3倍するオブジェクト
cout << "5の2倍 = " << nibai( 5 ) << "\n";
cout << "15の3倍 = " << sanbai( 15 ) << "\n";
}
実行結果は下記のようになります。
5の2倍 = 10 15の3倍 = 45
(14)ラムダ式
ラムダ式は「無名関数」または「匿名関数」の表現法のひとつです。
[キャプチャリスト](パラメータリスト) mutable 例外仕様 属性 -> 戻り値の型 { 関数の本体 }
キャプチャとは、ラムダ式定義時に必要な変数のコピーまたは参照をキャプチャしておき、
実行時にそれらの値を用いて演算します。
コピーのときは問題ないのですが、参照を用いるときは実行時にその参照が有効である必要があります。
万が一参照が無効(寿命が尽きた)になってしまったときの実行結果は不定になりますので注意してください。
| キャプチャ記法 | 説明 |
|---|---|
| [&] | デフォルトで環境にある変数を参照します |
| [=] | デフォルトで環境にある変数をコピーします |
| [&x] | 変数xを参照します |
| [x] | 変数xをコピーします |
| [&, x] | デフォルトで参照キャプチャ、変数xのみコピーします |
| [=, &x] | デフォルトコピーキャプチャ、変数xのみ参照します |
| [this] | *thisのメンバを参照します |
| [this, x] | *thisのメンバを参照し、変数xのみコピーします |
以下にラムダ式のサンプルプログラムを示します。
#include <iostream>
using namespace std;
int main()
{
int ml;
ml = 2;
// 注意:実行時、mlは定義時のコピーが使用されます。
// nibaiのml(のコピー)は2になります。
auto nibai = [=]( int x ) { return ml * x; };
ml = 3;
// 注意:実行時、mlは定義時のコピーが使用されます。
// sanbaiのml(のコピー)は3になります。
auto sanbai= [=]( int x ) { return ml * x; };
cout << "5の2倍 = " << nibai( 5 ) << "\n";
cout << "15の3倍 = " << sanbai( 15 ) << "\n";
}
実行結果は下記のようになります。
5の2倍 = 10 15の3倍 = 45
ラムダ式がデフォルト引数に現れる場合には、いかなるキャプチャもしてはいけません。
余談ですが、ラムダ式の後に()を付けると直ちに実行されます。
#include <iostream>
using namespace std;
void f4( int a )
{
cout << a << "\n";
}
void f5( int a )
{
cout << a << "\n";
}
// ラムダ式がデフォルト引数に現れる場合には、いかなるキャプチャもしてはいけません!
int main()
{
int x = 3;
// ローカル関数の宣言
// void f1(int = ([x]{ return x; })()); // コンパイルエラー
// void f2(int = ([x]{ return 0; })()); // コンパイルエラー
// void f3(int = ([=]{ return x; })()); // コンパイルエラー
void f4(int = ([=]{ return 1; })()); // OK : デフォルトキャプチャしたが、使用していない
void f5(int = ([]{ return sizeof(x); })()); // OK : キャプチャなし}
f4();
f5();
}
実行結果は下記のようになります。
1 4
パラメータパックをキャプチャする際は、...で展開します。
#include <iostream>
using namespace std;
template <class First>
void bar( First first )
{
cout << first << "\n";
}
template <class First, class... Args>
void bar( First first, Args... rest )
{
cout << first << "\n";
bar( rest... );
}
template <class... Args>
void foo( Args... args )
{
auto f = [args...] { bar(args...); };
f();
}
int main()
{
foo( 3, 10.5, "String" );
}
実行結果は下記のようになります。
3 10.5 String
パラメータリストの制限
ラムダ式のパラメータには、テンプレートは使用できません。そのため、具体的な型を指定する必要があります。
(15)alignas
コンパイラに対し変数をメモリ上の特定の位置に配置(アライメント)するように要求するキーワードです。
変数やクラスのメンバ変数宣言時に使用した場合、宣言した変数をアライメントします。
alignas(32) int i; //32バイト境界にアライメントする
構造体やクラスの宣言時に使用した場合、その型のインスタンス全てをアライメントします。
struct alignas(32) hoge {
};
hoge a; //32バイト境界にアライメントする
hoge b; //32バイト境界にアライメントする
alignas(定数)は、定数の値でアライメントをします。0または0として評価される値を指定したときは何も起きません。
alignas(型)は、alignas(alignof(型))と等価です。
ある型 A のアライメントを別の型 B にも適用したいときalignas(A) B hoge;のように書けます。
(16)alignof
指定した型がメモリ上のどの位置に配置されるか(アライメント)取得する演算子です。
#include <cstddef>
#include <iostream>
struct hoge {
char c[63];
short s;
double i;
};
struct empty {
};
int main()
{
std::cout <<
"std::max_align_t: " << alignof(std::max_align_t) << std::endl <<
"char: " << alignof(char) << std::endl <<
"int: " << alignof(int) << std::endl <<
"double: " << alignof(double) << std::endl <<
"struct hoge: " << alignof(hoge) << std::endl <<
"struct empty: " << alignof(empty) << std::endl <<
"char *: " << alignof(char *) << std::endl;
}
実行結果の例(以下の値は実行する環境により変わることがあります)。
std::max_align_t: 8 char: 1 int: 4 double: 8 struct hoge: 4 struct empty: 1 char *: 4