この章では、C++の根本的なルールを解説する。この章の内容は、言語の深い詳細について解説しているので、すべてを理解する必要はない。必要に応じて参照すればよい。
C++には、名前という概念が存在する。変数や関数、クラス等には、名前をつけられる。名前を使うには、必ず、あらかじめ、その名前が宣言されていなければならない。
宣言とは、ある名前が、その翻訳単位で、何を意味するのかということを、明示するためにある。
定義とは、名前の指し示すものを、具体的に記述することである。宣言と定義は、多くの場合、同時に行われることが多いので、あまり意識しづらい。
// これは関数の宣言
void f( ) ;
// これは関数の宣言と定義
void f( ) { }
以下の例は、関数を宣言だけして、具体的な定義をせずに、使っている。
// 関数の宣言
int f( int ) ;
int main()
{
int x = f( 0 ) ;
}
これは、問題がない。なぜならば、関数を使うには、引数や戻り値の型などが決まってさえいればよいからだ。この場合、その関数を指し示す名前として、fが使われている。この関数 int f( int )の、具体的な実装、つまり、「定義」は、同じ翻訳単位になくても構わない。つまり、int f( int )は、別のソースコードで定義されているかもしれない。
すべての定義は、宣言である。宣言は、定義ではない場合もある。定義ではない宣言は、以下の通りである。
関数宣言で、関数の本体がない場合。
// 宣言
void f( int ) ;
// 宣言と定義
void f( int ) { }
extern指定子を使っていて、初期化子も、関数の本体も記述されていない宣言。
// 宣言
extern int x ;
// 宣言と定義
int x ;
リンケージ指定されていて、初期化子も、関数の本体も記述されていない宣言。ただし、{}の中の宣言には、影響しない。
// 宣言
extern "C" void f() ;
extern "C"
{
// これは、宣言と定義
void f() { }
int x ;
// 宣言
void g() ;
extern int y ;
}
クラス名の宣言。
// クラス名の宣言
class C ;
// クラスの宣言と定義
class C { } ;
クラス定義の中の、staticなデータメンバーの宣言。
class C
{
// 宣言
static int x ;
} ;
// 定義
int C::x ;
enum名の宣言
// 宣言
enum E ;
// 宣言と定義
enum E { up, down } ;
typedef宣言
// 宣言
typedef int type ;
using宣言と、usingディレクティブ
namespace NS { void f(){} }
// 宣言
using NS::f ;
using namespace NS ;
また、static_assert宣言、アトリビュート宣言、空宣言は、定義ではない。
static_assert( true, "" ) ; // 宣言
[[ ]] ; // 宣言
; // 宣言
ODR(One definition rule)とは、定義は原則として、ひとつしか書けないというルールである。
多くの場合、同じ宣言は、いくつでも書ける。ただし、変数、関数、クラス型、enum型、テンプレートの、同じ定義は、ひとつしか書くことができない。
// 同じ宣言はいくつでも書ける。
void f() ; void f() ; void f() ; void f() ;
// 定義はひとつしか書けない。
void f() { }
// エラー、定義が重複している
void f() { }
定義は、プログラムのすべての翻訳単位で、一つでなければならない。なぜ定義はひとつしか書けないのか。定義が複数あると、問題があるからだ。
// 定義が二つある。
int x ;
int x ;
// どっちのx?
x = 0 ;
// 定義が二つある
void f() { }
void f() { }
// どっちのf()?
f() ;
このような問題を防ぐために、定義は、原則として一つでなければならないとされている。
原則としてというのは、例外があるのだ。もし、本当に、定義を一箇所でしか書けないと、困ることがある。たとえば、クラスだ。
// 翻訳単位1 A.cpp
// 定義
struct C
{
int x ;
} ;
C c ; // OK
// 翻訳単位2 B.cpp
// 宣言
struct C ;
C c ; // エラー
翻訳単位2で、クラスCの変数を定義するためには、クラスCは、定義されていなければならない。しかし、すでに、別の翻訳単位で、定義は書かれている。B.cppにも定義を書いてしまうと、ODRに違反する。これは一体、どうすればいいのか。
このため、C++では、クラス型、enum型、外部リンケージを持つインライン関数、クラステンプレート、外部リンケージを持つ関数テンプレート、クラステンプレートのstaticデータメンバー、クラステンプレートのメンバー関数、具体的な型を完全に指定していないテンプレートの特殊化に限り、ある条件を満たせば、別の翻訳単位での、定義の重複を認めている。ある条件とは何か。これには、大きく分けて、二つある。
同じ定義のソースコードは、全く同じトークン列であること。
// 翻訳単位1 A.cpp
struct C
{
int x ;
} ;
// 翻訳単位2 B.cpp
struct C
{
public : // エラー。
int x ;
} ;
ここで、翻訳単位2に、public :があろうとなかろうと、意味は変わらない。しかし、全く同じトークン列ではないので、このプログラムはエラーである。
全く同じ複数の定義を管理するのは、極めて困難である。そのため、このように翻訳単位ごとに定義しなければならないクラスやテンプレートは、通常、ヘッダーファイルに記述して、必要な翻訳単位ごとに、#includeされる。
// ヘッダーファイル C.h
struct C
{
int x ;
} ;
// 翻訳単位1 A.cpp
#include "C.h"
C c ;
// 翻訳単位2 B.cpp
#include "C.h"
C c ;
定義の意味が、プログラム中のすべての翻訳単位で、同じであること。
定義のソースコードが、全く同じトークン列であるからといって、意味も同じであるとは限らない。
// ヘッダーファイル C.h
class C
{
void member()
{
f() ; // fという名前の、何らかの関数を呼び出す。
}
} ;
このクラス、Cは、member()というメンバー関数で、f()という関数を呼び出している。では、このクラスを使うコードが、以下のようであれば、どうなるか。
// 翻訳単位1 A.cpp
namespace A
{ void f() {} }
// f()はA::f()を呼び出す
using A::f ;
#include "C.h"
// 翻訳単位2 B.cpp
namespace B
{ void f() {} }
// f()はB::f()を呼び出す
using B::f ;
#include "C.h"
ヘッダーファイルによって、クラスCのソースコードのトークン列は、全く同じなのに、この例では、呼び出す関数が翻訳単位ごとに変わってしまう。このようなコードはエラーである。プログラム中の同じ定義は、必ず同じ意味でなければならない。
宣言された名前には、その名前が有効に使える範囲が存在する。これを、宣言範囲(declarative region)、スコープ(scope)という。
int x ;
void f()
{
int y ;
{
int z ;
}
// ここではもう、zは使えない。
}
// ここではもう、yは使えない。
// xは、ここでも使える。
ある名前は、スコープの中ならば、必ず同じ意味であるとは限らない。名前は上書きされる場合がある。
void f()
{ // ブロック1
int x ; // #1
{ // ブロック2
int x ; // #2
x = 0 ; // #2が使われる。
}
x = 0 ; // #1が使われる。
}
この例では、ブロック1で宣言されたxは、ブロック2では、別の変数を指し示すxに、隠されている。
このように、スコープがネストする場合、外側のスコープの名前が、内側のスコープの名前に隠されてしまうことがある。
スコープには、いくつもの種類がある。これを詳しく説明する前に、まず、宣言された名前は、どこから有効なのかということを、明らかにしておかなければならない。この、名前が有効になる始まりの場所を、宣言場所(Point of declaration)という。名前は、宣言のすぐ直後から有効になる。
int x ; // 宣言場所
// ここから、xが使える。
宣言場所は、初期化子よりも、前である。
int x /*ここから名前xは有効*/ = x ;
この例では、xという変数を宣言して、その変数の値で初期化している。このコードに実用的な意味はない。初期化子の中から、宣言された名前は使えるということを示すためだけの例である。
// エラー
int x[x /*ここでは、まだxは未定義*/ ] ;
この例は、エラーである。なぜなら、配列の要素数を指定する場所では、xは、まだ定義されていないからだ。これらの例は、通常は気にすることはない、些細な詳細である。一般に、宣言文のすぐ後から使えると考えておけばいい。
ブロックのスコープは、そのブロックの中である。これを、ブロックスコープと呼ぶ。よく、ローカル変数と呼んでいるものは、ブロックスコープの中で宣言された変数のことである。
void f()
{ // ブロック1
int x ;
{ // ブロック2
int y ;
{ // ブロック3
int z ;
// x, y, zが使える
}
// x, yが使える。
}
// xが使える。
}
// ここで使える変数名はない。
ブロックはネストできるので、ネストされたブロックの中で、外側のスコープと同じ名前の変数を使いたい場合は、注意が必要である。
void f()
{
int x ;
{
int x ; // 外側のスコープのxは隠される。
}
}
関数の仮引数名は、関数本体の一番上のブロックスコープの終わりまで、有効である。
void f( int x )
{
// xはここまで有効
}
// これ以降、xは使えない。
関数のプロトタイプ宣言にも、スコープがある。関数のプロトタイプ宣言のスコープは、その宣言の終わりまでである。
auto f( int x ) -> decltype(x) ;
この例では、仮引数の名前が、decltypeに使われている。
ブロックスコープではなく、関数自体にも、関数のスコープが存在する。これは、ある関数全体のスコープである。ただし、この関数のスコープが適用されるのは、ラベル名だけである。
void f()
{
{
label : ;
}
goto label ; // labelは、ここでも有効
}
このように、ラベル名には、関数のスコープが適用される。
名前空間のスコープというのは、少しややこしい。まず、名前空間の本体は、もちろんスコープである。
namespace NS
{
int x ;
// xが使える。
}
// ここでは、xは使えない。
この、名前空間の中の名前(上の例では、x)を、名前空間のメンバー名という。メンバー名のスコープは、名前空間の終わりまでである。
ところが、名前空間の本体の定義は、複数書くことができる。
namespace NS
{
int x ;
// xが使える。
}
// ここでは、xは使えない。
namespace NS
{
// ここでも、xが使える。
int y = x ;
}
メンバー名は、その宣言された場所から、後続するすべての同名の名前空間の中で使うことができる。この例の場合、二つめの名前空間NSの定義の中でも、一つめの名前空間NSの定義で宣言されたメンバー名である、xを使うことができる。
名前空間のメンバーは、スコープ解決演算子、::を使って、参照することもできる。
namespace NS
{
using type = int ;
}
// 名前空間NSの、typeという名前を参照している。
NS::type x ;
翻訳単位の、一番上の、namespaceで囲まれていない場所も、一種の名前空間として扱われる。これは、グローバル名前空間と呼ばれている。グローバル名前空間で定義された名前は、グローバル名前空間のスコープに入る。これは、グローバルスコープとも呼ばれている。グローバル名前空間のスコープは、翻訳単位の終わりまでである。
// グローバル名前空間
int x ;
namespace NS
{ // 名前空間、NS
}
// ここは、グローバル名前空間
namespace
{ // 無名名前空間
}
// ここも、グローバル名前空間
// xの範囲は、翻訳単位の終わりまで続く。
クラスのスコープは、少し変わっている。ブロックスコープなどは、名前の有効な範囲は、名前を宣言した場所から、スコープの終わりまでである。
void f()
{
// ここでは、xは使えない。
int x ; // xを宣言
// ここでは、xを使える。
}
クラスでは、これが変わっている。
先に、名前が宣言されていなくても、クラス内の関数からは、その名前を使うことができる。
class C
{
void f()
{ // 関数の中で、名前を使うことができる。
type x ;
value = 0 ;
}
type y ; // エラー。typeは宣言されていない。
using type = int ; // typeの宣言場所
type z ; // OK
int value ; // valueの宣言場所
} ;
また、クラスのメンバー関数を、クラスの外部で定義する場合でも、その関数の中から、クラス内で宣言された名前を使うことができる。
class C
{
void f() ;
int x ;
} ;
void C::f()
{ // クラス外部で定義されたメンバー関数の中で、クラス内で宣言された名前を使える。
x = 0 ;
}
その他にも、クラス内の名前を、クラス外で使うことができる場合が存在する。
class C
{
public :
int x ;
using type = int ;
} ;
int main()
{
C c ;
C * p = &c ;
// クラスのメンバーアクセス演算子の後に続けて、名前を使える。
c.x = 0 ;
p->x = 0 ;
// スコープ解決演算子の後に続けて、名前を使える。
C::type value ;
}
このように、クラススコープの名前は、宣言した場所から、ある区間まで有効というルールではない。このため、クラスのスコープには特別なルールがある。
- クラスのメンバーの宣言が全てわかったあとに、クラス宣言を再評価して、プログラムの意味が変わるとエラー
- クラス内のメンバーの宣言の順番を変えた際に、プログラムの意味が変わると、エラー
これは、例をあげて説明したほうが分かりやすい。今仮に、このルールがないものとする。とすると、以下のようなコードが書けてしまう。
// コード1
using type = int ; // #1
class C
{
type x ; // このtypeは、#1の::type
using type = float ; // #2
} ;
クラスCの宣言の順番を変えると、以下のコードになる。
// コード2
using type = int ; // #1
class C
{
using type = float ; // #2
type x ; // このtypeは、#2の、C::type
} ;
このように、メンバーの宣言の順番を変えることによって、プログラムの意味が変わってしまうと、意図せぬバグを生む原因となる。そのため、このようなコードは、エラーである。
scoped enumは、enumスコープ(enumeration scope)を持つ。このスコープの範囲は、enumの宣言内だけである。
enum class E { x, y, z } ;
// ここで、x, y, zは使えない。
x ; // エラー
E::x ; // OK
この理由は、scoped enumは、強い型付けを持つenumだからだ。詳しくは、enumを参照のこと。
テンプレート仮引数にも、スコープがある。テンプレート仮引数のスコープは、それほど意識する必要はない。
template <
typename T, // これ以降、Tを使える。
typename U = T >
class C { } ; // テンプレート仮引数のスコープ、ここまで
ただし、テンプレート仮引数名は、基本的に、隠すことができない。
template < typename T >
class C
{
using T = int ; // エラー
// エラー
template < typename T >
void f() ;
} ;
「基本的に」というのは、隠すことができる場合も存在するからだ。
struct Base{ using T = type ; } ;
template < typename T >
struct Derived : Base
{
T x ; // Base::Tが使われる。テンプレート仮引数ではない。
} ;
といっても、これはよほど特殊な例であり、通常は、テンプレート仮引数名は、隠せないと考えても、問題はない。
ネストされたスコープの内側で、同じ名前が宣言されると、外側の名前は、隠される。
void f()
{ // 外側のスコープ
int x ;
{ // 内側のスコープ
int x ; // 外側のスコープのxを隠す。
x = 0 ; // 内側のx
}
x = 0 ; // 外側のx
}
派生クラスでは、基本クラスの名前は隠される。
struct Base { using type = char ; } ;
struct Derived : Base
{
using type = int ;
type x ; // int
} ;
クラスやenumの名前は、変数やデータメンバーの名前によって、隠される。
class ClassName {} ;
void f()
{
ClassName ClassName ; // OK、ClassName型の変数、ClassName
ClassName x ; // エラー、ClassNameは、ここでは変数名を指す。
class ClassName x ; // OK、明示的にクラス名であると指定している。
}
このように、クラス名と変数名を同じにするのは、非常に分かりにくい問題を引き起こすので、あまりおすすめできない。
あるスコープにおいて、ある名前が使われているとき、その名前が何を意味するのかということを決定するのを、名前探索(Name lookup)と呼ぶ。これは一見簡単そうに思える。しかし、この名前を決定するというルールは、非常に難しい。
Name lookupには、大きく分けて、三種類ある。Qualified name lookup、Unqualified name lookup、Argument-dependent name lookupだ。
Qualified nameとは、qualified(修飾)という名前通り、スコープ解決演算子(::)を使った名前のことである。
int g ;
namespace NS { int x ; }
struct C { static int x ; } ;
int C::x ;
enum struct E { e } ;
int main()
{
// これらはQualified name lookup
NS::x ; // NSという名前空間のx
C::x ; // Cというクラスのx
E::e ; // Eというenumのメンバー、e
::g ; // グローバル名前空間のg
}
このような名前に対する名前探索を、Qualified name lookupという。
スコープ解決演算子(::)の左側には、クラス名か、名前空間名か、enum名を書くことができる。左側に何も書かない場合、グローバル名前空間が使われる。Qualified name lookupでは、名前は、スコープ解決演算子で指定された、クラスや名前空間、enum内の名前から、探索される。
スコープ解決演算子は、ネストできる。
namespace N1 { namespace N2 {
int x ;
} }
N1::N2::x ;
Unqualified(非修飾) name lookupは、Qualified name lookup以外を指す。これはつまり、スコープ解決演算子を使わない名前に対する、名前探索である。
int g ;
namespace NS { int x ; }
int main()
{
g ; // グローバル変数のg
int g ;
g ; // ローカル変数のg
{
using namespace NS ;
x ; // NS::xと同じ
}
{
using NS::x ;
x ; // NS::xと同じ
}
}
Unqualified nameに対する名前探索を、Unqualified name lookupという。Unqualified name lookupでは、その名前が使われている場所で、明示的に修飾しなくても、見つかる名前が探される。これは、例えばグローバル名前空間内の名前であったり、クラス内であれば、クラスのメンバーであったりする。また、using directiveや、using declarationの影響をうける。
Unqualified nameに対して、関数呼び出しをする場合、特別なルールがある。このルールを、ADL(Argument-dependent name lookup)という。
namespace NS
{
class C {} ;
void f( C ) {}
}
int main()
{
NS::C c ;
f(c) ; // NS::fを呼ぶ
}
このコードでは、通常は見つからないはずの、NSという名前空間内の関数であるfが、Unqualified nameなのにもかかわらず、見つかる。これを、実引数に依存する名前探索(Argument-dependent name lookup)と呼ぶ。しばしば、ADLと略される。また、Andrew Koenigさんが、名前空間の導入によって、特に演算子のオーバーロードで、ADLのような必要性を意見したため、Koenig lookupとも呼ばれることがある。Andrew Koenigさんが、ADLの具体的な仕組みを考案したわけではない。誰がADLの原案を考えだしたのかは、歴史に埋もれて忘れ去られているが、そのような歴史的な経緯と誤解により、Koenig lookupと呼ばれている。
このADLというルールは、一見すると、非常に奇妙なルールである。このような仕組みは、非常に厄介な問題を引き起こすのではないか。事実、ADLは時として、問題になることがある。それでもADLが存在するのは、利点があるからだ。
整数を表現するクラス、Integerを考える。名前の衝突を防ぐため、このクラスは、libという名前空間の中に入れたい。また、整数として分かりやすく使うために、演算子をオーバーロードしたい。Integerクラスは、以下のように使えるものとする。
int main()
{
lib::Integer x ;
// 演算子のオーバーロードによる、分かりやすい加算のコード。
x + x ;
}
さっそく、このIntegerを実装してみよう。
namespace lib
{
// クラス
class Integer { /*実装*/ } ;
// 演算子のオーバーロード
Integer operator + ( Integer const &, Integer const &)
{
// 実装
return Integer() ;
}
}
もしここで、ADLがない場合、operator +()の呼び出しが、困ったことになる。なぜなら、Unqualified lookupでは、lib名前空間の中の名前を探してはくれない。つまり、operator +は、見つからないのである。
lib::Integer x ;
// エラー、operator + が見つからない。
x + x ;
ではどうするか。これは、Qualified lookupを使うしかない。
lib::operator +( x, x ) ;
このコードは動く。確かに動くが、これでは、せっかく演算子をオーバーロードした意味がない。そもそも、演算子をオーバーロードする理由とは、x + x という、分かりやすい使い慣れたコードを書くためだからだ。
このため、Unqualified nameに対する、関数呼び出しには、Unqualified name lookupに加えて、ADLという仕組みで、名前が探索されるようになっている。
ADLは、その名前が示すとおり、「実引数に依存する名前解決」である。どの名前空間から、名前を探すかということは、実引数の型から決定される。また、ADLは、必ず行われるわけではない。ADLが適用される条件というものが存在する。
ADLはどのように行われるか。まず、関数に対する、関連クラス(Associated class)と、関連名前空間(Associated namespace)というものが決定される。ADLは、この関連名前空間の中から、名前を探索する。
関連クラスとは、関数に実引数として渡される型である。関連名前空間とは、関連クラスがメンバーとなっている名前空間である。
namespace NS
{
class A {} ; class B {} ; class C {} ; class D {} ;
void f( A, B, C, D ) {}
}
int main()
{
NS::A a ; NS::B b ; NS::C c ; NS::D d ;
f( a, b, c, d ) ;
}
この場合、fの関数呼び出しに対する関連クラスは、A、B、C、Dで、関連名前空間は、NSとなる。
namespace A { class C {} ; }
namespace B
{
class C {} ;
void f( A::C, B::C ) {}
}
int main()
{
A::C ac ; B::C bc ;
f( ac, bc ) ;
}
この場合、fの関数呼び出しに対する関連クラスは、A::C、B::Cで、関連名前空間は、A、Bとなる。
実引数の型のクラスの、基本クラスも、関連クラスになる。
namespace NS
{
class A {} ;
class B : A {} ;
class C : B {} ;
void f( C ) {}
}
int main()
{
NS::C c ;
f( c ) ;
}
この場合、関数NS::fに対する関連クラスは、A、B、Cで、関連名前空間は、NSとなる。
実引数がクラステンプレートであった場合、そのクラスのテンプレート実引数も、関連クラスになる。
namespace NS
{
template < typename T > class C {} ;
}
namespace lib
{
class type {} ;
template < typename T >
void f( NS::C<T> ) {}
}
int main()
{
NS::C< lib::type > c ;
// 関連クラスは、NS::C< lib::type >と、lib::type。
// 関連名前空間は、NSと、lib。
f(c) ; // lib::fを呼び出す。
}
テンプレート実引数も関連クラスになるというルールは、この例のような、非常に分かりにくいコードのコンパイルを通してしまう。
実引数がクラス以外の場合も、ADLは適用される。
実引数がenumの場合、そのenumが定義されている名前空間が、関連名前空間になる。
namespace NS
{
enum struct E { value } ;
void f( E ) {}
}
int main()
{
f( NS::E::value ) ; // NS::fを呼び出す。
}
この場合、関数、NS::fの関連名前空間は、NSとなる。
ADLが適用されるには、条件を満たさなければならない。まず、ADLは、Unqualified nameへの関数呼び出しにしか、適用されない。変数としての使用には、ADLは使われない。
namespace NS
{
class C {} ;
void f( C ) {}
void g( C ) {}
}
void g( NS::C ) {}
int main()
{
NS::C c ;
f(c) ; // ADLで、NS::fを呼ぶ
NS::f(c) ; // Qualified name lookupが行われる
::g(c) ; // Qualified name lookupが行われる
NS::g(c) ; // Qualified name lookupが行われる
g(c) ; // エラー。::g、NS::gのどちらの名前か、曖昧。
}
最後の例は、Unqualified name lookupで、::gが発見され、ADLで、NS::gが発見されるので、どちらの名前を使うのか、曖昧で、エラーになる。
もし、Unqualified name lookupで、関数名以外の名前が見つかった場合、ADLは行われない。
namespace NS
{
class C {} ;
void f( C ) {}
}
int f ;
struct Caller
{
void f( NS::C ) {}
void g()
{
NS::C c ;
f(c) ; // Caller::fが呼ばれる。ADLは行われない。
}
} ;
int main()
{
NS::C c ;
f(c) ; // エラー。fはint型の変数
}
ブロックスコープ関数宣言の名前が見つかった場合、ADLは行われない。ただし、using宣言や、usingディレクティブは、影響しない。
namespace NS
{
class C {} ;
void f( C ) {}
}
namespace lib { void f( NS::C ) {} }
int main()
{
NS::C c ;
{
void f( NS::C ) ; // ブロックスコープの関数宣言
f(c) ; // ::fを呼び出す。ブロックスコープの宣言が見つかったので、ADLは行われない。
}
{
using namespace lib ;
f(c) ; // エラー、ADLも行われるので、曖昧になる。
}
{
using lib::f ;
f(c) ; // エラー、ADLも行われるので、曖昧になる。
}
}
// ブロックスコープの関数宣言で参照される、グローバル名前空間のf
void f( NS::C ) {}
ブロックスコープ内で関数宣言をするということは、言語上は認められているが、現実的には、あまり用いられていない。
using宣言や、usingディレクティブが、ADLの適用を妨げないということは、注意を要する。これにより、不思議なコンパイルエラーになることがある。例えば、上の例の場合、NS名前空間のコードは、他人が書いたものであり、ユーザーはよく知らないとしよう。lib名前空間のコードは、ユーザーが書いたものである。ユーザーは、lib::fを使いたい。main関数内で多用するので、using宣言を使って、簡単に呼び出せるようにした。ところが、NS名前空間の中にも、同名の関数があるので、曖昧エラーになってしまう。
ADLが意図せず適用された際のエラーは、非常に分かりにくい。そのため、ADLを防ぐための方法が用意されている。名前を括弧で囲めば、ADLの適用が阻害される。
namespace NS
{
class C {} ;
void f( C ) {}
}
void f( NS::C ) {}
int main()
{
NS::C c ;
f(c) ; // エラー。曖昧
(f)(c) ; // OK、ADLは適用されない。::fを呼び出す。
}
unqualified nameへの関数呼び出しは、通常のunqualified name lookupと、ADLとで見つかった名前の、両方が用いられる。
C++において、プログラム(program)とは、ひとつ以上の翻訳単位(translation unit)が組み合わさったものである。翻訳単位というのは、一連の宣言で構成されている。
よくあるC++の実装としては、翻訳単位は、ソースファイルという単位で分けられている。
同じオブジェクト、リファレンス、関数、型、テンプレート、名前空間、値を指し示す名前が、宣言によって別のスコープに導入された時、その名前はリンケージ(linkage)を持つ。
ある名前がリンケージを持つのか持たないのか、リンケージを持つ場合、外部リンケージなのか内部リンケージなのかというルールは複雑である。
基本的な考え方としては、グローバル名前空間を含む名前空間スコープの名前は、ほとんどが外部リンケージを持つ。内部リンケージを持つものは、大抵、明示的な指定子を伴う。リンケージを持たないエンティティは、そもそも名前を持たない場合が多い。
名前空間スコープの名前で、内部リンケージを持つものは、以下の通り。
-
明示的にstaticと宣言されている変数、関数、関数テンプレート
static int x ;
static void f() { }
template < typename T >
static void f() { }
-
明示的にconstかconstexprと宣言されていて、明示的にexternと宣言されておらず、前方で外部リンケージをもつと宣言されてもいないもの
const int x = 0 ;
constexpr int y = 0 ;
-
無名unionのデータメンバー
static union { int x ; } ;
無名名前空間か、無名名前空間内で、直接的、間接的に宣言された名前空間は、内部リンケージを持つ。
// この無名名前空間は内部リンケージを持つ
namespace { }
namespace
{
// 名前空間internalは内部リンケージを持つ
namespace internal
{
// 名前空間indirectは内部リンケージを持つ
namespace indirect { }
}
}
これ以外の名前空間は、すべて外部リンケージを持つ。例えば、グローバル名前空間も外部リンケージを持つ。
名前空間スコープを持つ、次に挙げる種類の名前はすべて、属する名前空間スコープと同じリンケージを持つ。
-
変数
-
関数
-
名前のあるクラス、もしくは、typedef宣言によって定義された無名クラスで、typedef名をもつクラス
// グローバル名前空間
// 外部リンケージを持つ
struct Named { } ;
// 外部リンケージを持つ
typedef struct { } Typedef_named ;
-
名前のあるenum、もしくは、typedef宣言によって定義された無名enumで、typedef名をもつもの
-
列挙子は、その属するenumのリンケージに従う
// fooのリンケージはEのリンケージと同じ
enum E { foo } ;
-
テンプレート
つまり、名前空間のリンケージに従う。
// グローバル名前空間
namespace ns
{
int foo ; // 外部リンケージ
}
namespace
{
int bar ; // 内部リンケージ
}
メンバー関数、staticデータメンバー、クラススコープの名前のあるクラスやenum、クラススコープのtypedef宣言で定義された無名クラスや無名enumでtypedef名をもつものは、その属するクラスが外部リンケージを持つ場合、外部リンケージを持つ。
ブロックスコープで定義された、関数名と、extern宣言された変数名は、リンケージを持つ。
void f()
{
extern void g() ; // リンケージを持つ
extern int global ; // リンケージを持つ
g() ; // OK
global = 123 ; // OK
}
// 別の翻訳単位
void g() { }
int global ;
もし、ブロックスコープの外の直前の名前空間スコープで、同じ名前、同じエンティティでリンケージだけが違う前方宣言がなされていた場合、ブロックスコープのリンケージ指定は無視され、前方宣言のリンケージが使われる。
// グローバル名前空間
static void f(); // 内部リンケージ
static int i = 0; // 内部リンケージ
void g() {
extern void f(); // 内部リンケージ
int i; // リンケージなし
{
extern void f(); // 内部リンケージ
extern int i; // 外部リンケージ
}
この例では、名前iのオブジェクトは3つ存在する、それぞれ、内部リンケージ、リンケージなし、外部リンケージをもつ、別のオブジェクトを指し示している。
もし、ブロックスコープの名前と、前方宣言された名前空間スコープの名前で、名前に適合するエンティティが複数あった場合、エラーとなる。そのような重複がない場合、ブロックスコープのextern指定した名前は、外部リンケージを持つ。
ブロックスコープで宣言された名前でリンケージを持つものが、すでに前方宣言されていない場合は、ブロックスコープを包む直前の名前空間のメンバーとなる。ただし、名前空間スコープのメンバーの名前として現れることはない。
namespace ns
{
void f()
{
extern void h() ; // 名前空間nsのメンバーとなる
h() ; // OK
}
void g()
{
h() ; // エラー、名前hは宣言されていない
}
void h() { } // ns::hの宣言と定義
}
// これはns::hとは関係がない、別のエンティティ
void h() { }
ここまでの条件に当てはまらなかった名前は、リンケージを持たない。特に、ブロックスコープで宣言された名前のほとんどは、すでに上げた条件にあてはまらない限り、リンケージを持たない。
型のうち、リンケージを持つものは以下の通り。
-
名前を持つクラス型、もしくはenum型、typedef宣言内で定義された無名クラス型や無名enum型だが、typedef名を持つもので、その名前がリンケージを持つもの
-
リンケージを持つクラスの中の、無名クラスと無名enumのメンバー
-
クラステンプレートの特殊化
template < typename T >
void f() { }
void g()
{
f<int>() ;
}
この例では、f<int>のみがリンケージを持つ。f<short>やf<double>のような型は、特殊化されていないので、リンケージを持たない
-
基本型
-
クラスとenum以外の複合型で、リンケージを持つ型が複合しているもの。
たとえば、int *やint **と言った型リンケージを持つ
-
リンケージをもつ型がCV修飾された型
リンケージを持たない型は、リンケージを持つ関数やクラスで使うことはできない。ただし、以下の場合は使うことができる。
-
エンティティがC言語リンケージをもつ場合
-
エンティティが無名名前空間で宣言されている場合
-
エンティティがODRの文脈で使われていない場合、もしくは、同じ翻訳単位で宣言されている場合
異なるスコープで宣言された同一の二つの名前は、以下の条件をすべて満たした場合、同じ変数、関数、型、enum、テンプレート、名前空間を指し示す。
-
名前は両方とも、ともに外部リンケージを持つか、ともに内部リンケージを持ち、同じ翻訳単位で宣言されている。
-
名前は両方とも、同じ名前空間のメンバーか、派生によらない同じクラスのメンバーを指し示している
-
名前が両方とも関数を指し示す場合は、関数の仮引数の型リストが同一であること
-
名前が両方とも関数テンプレートを指し示す場合は、シグネチャが同じであること
NOTE:ここから先のBasic Conceptsは、最新のC++14ドラフト、N3797を参照している。ここから先はC++11とC++14の差が激しく、最新のドラフトを参考することにした。
プログラム中にある、mainという名前のひとつのグローバル関数から、プログラムは開始する。freestanding環境でmainの定義を必要とするかどうかは実装依存である。ホスト環境と違い、freestanding環境では、開始と終了も実装依存となる。
main関数は特別な扱いを受ける。C++の実装は、mainを予め定義してはならない。main関数はオーバーロードできない。戻り値の型はintであるが、main関数自体の型は実装依存となる。
規格では、C++の実装は、以下の二つの形のmain関数を受け付けるように規定されている。
-
関数で、仮引数が()で、intを返すもの
int main() ;
auto main() -> int ;
-
関数で、仮引数が( int, charへのポインターへのポインター )で、intを返すもの
int main( int, char ** ) ;
// 仮引数に配列型を書くと、ポインター型になる。
int main( int, char *[] ) ;
2つ目の形では、慣習的に、関数の1つ目の仮引数はargcと呼ばれていて、2つ目の仮引数はargvと呼ばれている。argcはプログラムの実行される環境で、プログラムに渡された引数の数を表す。argcがゼロでなければ、引数はargv[0]から、argv[argc-1]まで、null終端されたマルチバイト文字列の先頭文字へのポインターを指す。argv[0]は、プログラムが実行された時のプログラムの名前か、あるいは""となる。argcの値が負数になることはない。argv[argc]の値は0となる。
#include <iostream>
int main( int argc, char ** argv )
{
std::cout << "Number of arguments: " << argc << std::endl ;
std::cout << "program name: " << argv[0] << std::endl ;
// 残りの引数
for ( std::size_t i = 1 ; argv[i] != nullptr ; ++i )
{
std::cout << argv[i] << std::endl ;
}
}
main関数は、プログラム中で使われてはならない。「使う」というのは、呼び出すことはもちろん、アドレスやリファレンスを取ることも含まれる。
auto ptr = &main ; // エラー
mainのリンケージは実装依存である。mainをdelete定義したり、inline, static, constexprなどと宣言することはエラーとなる。
mainという名前は、これ以外には予約されていない。つまり、クラス名やenum名や、クラスのメンバーをmainと名付けることは問題ないし、グローバル名前空間以外の名前空間でmainという名前を使うことも問題はない。
例えば、以下のコードは完全に合法なコードである。
class main { } ;
namespace ns
{
int main() { return 0 ; }
}
int main( ) { }
3つのmainという名前は、それぞれ別のエンティティを指す。
std::exitなどのような方法を使い、ブロックから抜け出さずにプログラムを終了させた場合、自動ストレージ上のオブジェクトは破棄されない。もし、std::exitがstaticやスレッドストレージ上のオブジェクトを破棄中に呼ばれたならば、プログラムの挙動は未定義である。
main関数の中のreturn文は、プログラムからの離脱の効果があり、自動ストレージ上のオブジェクトは破棄され、return文のオペランドの値が、std::exitへの実引数として渡される。もし、main関数の最後に到達しても、return文がない場合は、以下の文を実行したものと同等の効果になる。
return 0 ;
これはmain関数だけの特別な仕様である。
非ローカル変数には二種類ある。staticストレージ上の変数と、スレッドストレージ上の変数である。staticストレージ上の非ローカル関数は、プログラムの開始にともなって初期化される。スレッドストレージ上の非ローカル関数は、スレッドの実行にともなって初期化される。初期化は以下の手順で行われる。
staticストレージ上とスレッドストレージ上の変数は、他の初期化に先んじて、必ずゼロ初期化される。
変数に定数初期化子(constant initializer)がある場合、定数初期化(constant initialization)が行える場合は、行われる。定数初期化子とは、初期化子が定数式となる式の場合である。
ゼロ初期化と定数初期化をまとめて、静的初期化(static initialization)と呼ぶ。これ以外の初期化はすべて、動的初期化(dynamic initialization)である。静的初期化は、動的初期化の前に行われる。
staticストレージ上の非ローカル変数の動的初期化の順序があるものと、順序のないものがある。
明示的に特殊化されたクラステンプレートのstaticデータメンバーの初期化は、順序がある。暗黙、明示的に実体化された特殊化のstaticデータメンバーの初期化は、順序がない。
template < typename T >
struct S
{
static int data ;
} ;
// 実行時関数
int init()
{
static int count = 0 ;
return count++ ;
}
// 動的初期化される
template < typename T >
int S<T>::data = init() ;
// 明示的実体化
extern template struct S< int > ;
extern template struct S< short > ;
// 明示的特殊化
template < >
int S<double>::data = init() ;
template < >
int S<float>::data = init() ;
int main( )
{
S<long> s1 ;
S<long long int> s2 ;
}
クラステンプレートSのshort, int, long, long long intに対する特殊化は、暗黙や明示的に実体化されているので、staticデータメンバーdataの初期化の順序はない。そのため、値がどうなるかは、規格上定めることができない。
一方、floatとdoubleについての特殊化は、明示的に特殊化されているので、順序があり、doubleの特殊化がfloatの特殊化より先んじることが保証されている。
この他のstaticストレージ上の非ローカル変数は順序がある。
順序がある変数は、ひとつの翻訳単位の中では、定義されている順番通りに初期化される。
int init()
{
static int count = 0 ;
return count++ ;
}
int x = init() ; // 0
int y = init() ; // 1
int z = init() ; // 2
この他、規格では、挙動が変わらない場合、実装は動的初期化を静的初期化にしてもよいであるとか、main関数に処理が移った時点で初期化が完了している必要はないなどということを規定している。これは実装の選択や最適化を許すためのもので、コードから見える挙動は変わらない。
main関数からreturnするか、std::exitを呼び出した場合、staticストレージ上の初期化されたオブジェクトのデストラクターが呼び出される。スレッドの初期関数からreturnするか、std::exitを呼び出した場合、スレッドストレージ上の初期化されたオブジェクトのデストラクターが呼び出される。
終了時のオブジェクトの破棄中に、すでに破棄されたオブジェクトを参照した場合、挙動は未定義である。
規格上、ストレージという用語は正しくなく、ストレージの有効期間(storage duration)という用語が正しいのだが、本書では多くの箇所で、簡単にするため、単にストレージという言葉を使っている。
ストレージの有効期間には、様々なものがある。その具体的な実装方法は規定されていない。規格はC++の実装の選択肢をなるべく制限しないように書かれているので、細部は規定していない。たとえば、規格はC++のインタープリター実装を禁止していない。ここでは、よくある古典的な実装の一例が、各ストレージをどのように実装しているかを、簡単に記述するが、規格の規定ではないことに注意。
静的ストレージとも呼ばれるstaticストレージの有効期間(static storage duration)は、staticキーワードをつけて宣言したローカル変数やデータメンバー、thread_localをつけずに宣言した名前空間スコープの変数などが該当する。
既存のよくあるC++の実装では、静的ストレージは、プログラムのコードなどと一緒にバイナリ上に配置され、そのままメモリ上に読み込まれている。
スレッドストレージの有効期間(thread storage duration)は、thread_localキーワードをつけて宣言した変数が該当する。この変数は、スレッドごとに異なるオブジェクトが割り当てられる。スレッドの解説は本書の範疇ではないので、詳しくは説明しない。
既存のよくあるC++の実装では、スレッドストレージは、TLS(Thread Local Storage)などと呼ばれる、実装依存のスレッドごとに割り当てられた何らかのストレージ、あるいはストレージを指し示すハンドルなどから確保している。
自動ストレージの有効期間(automatic storage duration)は、ブロックスコープで明示的にregisterつきで宣言された変数、あるいは、staticやexternつきで宣言されていない変数が該当する。thread_localつきの変数宣言は暗黙的にstaticでもあるので、該当しない。
既存のよくあるC++の実装では、自動ストレージの有効期間は、スタックと呼ばれる特別なメモリから割り当てられる。
動的ストレージの有効期間(dynamic storage duration)を持つストレージ上のオブジェクトは、new式によって動的に作成され、delete式によって破棄される。
C++実装は、オブジェクトを構築する動的ストレージを確保するためのグローバルな確保関数(allocation function)と、動的ストレージを解放するための解放関数(deallocation function)を提供している。
確保関数は、operator newとoperator new[]で、解放関数はoperator deleteとoperator delete[]となる。
C++の標準ライブラリは、デフォルトの確保関数と解放関数の定義を提供している。これはオーバーロードして、独自の定義を与えることもできる。
確保関数、解放関数のシグネチャは、以下の通り。
void* operator new(std::size_t);
void* operator new[](std::size_t);
void operator delete(void*);
void operator delete[](void*);
void operator delete(void*, std::size_t) noexcept;
void operator delete[](void*, std::size_t) noexcept;
最後の二つの解放関数は、二番目の仮引数に、確保したストレージのサイズが渡される。
確保関数は、クラスのメンバー関数か、グローバル関数でなければならない。グローバル名前空間以外の名前空間で確保関数を宣言したり、グローバル名前空間でstaticとして確保関数を宣言すると、プログラムはエラーとなる。
確保関数の戻り値の型はvoid *でなければならない。最初の仮引数は、std::size_t型で、デフォルト実引数があってはならない。最初の引数には、確保関数が確保すべきストレージのサイズが渡される。
確保関数はテンプレート宣言できるが、戻り値の型と最初の仮引数は、前述通りでなければならない。確保関数のテンプレートは、二つ以上の仮引数を持たねばならない。
確保関数は、要求されたサイズ以上のストレージを確保できたならば、そのストレージの先頭アドレスを返す。ストレージの中身の値については、未規定である。
確保関数の返すポインターは、どの基本アライメント要求も満たすアドレスでなければならない。
確保関数がストレージの確保に失敗した場合、登録されているnewハンドラーがあれば呼び出す。確保関数が無例外指定されている場合は、nullポインターを返す。そうでなければstd::bad_alloc型の例外がthrowされる。
解放関数はクラスのメンバー関数か、グローバル関数でなければならない。グローバル名前空間以外の名前空間で解放関数を宣言したり、グローバル名前空間でstaticとして解放関数を宣言すると、プログラムはエラーとなる。
解放関数の戻り値の型はvoid、最初の仮引数は、void *でなければならない。
C++14では、解放関数が2個の仮引数を持ち、第二引数がstd::size_t型である場合、2つ目の仮引数には、確保したストレージのサイズが渡される。
解放関数から例外を投げて抜けだした場合、挙動は未定義である。
解放関数の1つ目の実引数がnullポインターでなければ、解放関数はポインターの指し示すストレージの解放を行う。
C++11にはガベージコレクションはないが、将来ガベージコレクションを追加することを見越して、安全なポインター(safely-derived pointer)というものを定義している。
ガベージコレクションとは、動的ストレージの明示的な解放をしなくても良くなる言語機能である。
ガベージコレクションの実装方法としては、プログラム中のポインターの値を検証し、どこからも参照されていない動的ストレージを探しだす方法がある。もし、あるストレージを参照する方法がなければ、そのストレージはもはや使われていないのだから、解放しても問題がないということになる。
しかし、C++は、ポインターに対する低級な操作を提供している。厳密には未定義の挙動になるが、ポインターの値をreinterpret_castでそのまま整数型に型変換して、整数型として演算したりできる。このような、ポインターの内部表現を変更して、後から元の内部表現に戻すような処理と、プログラム中の全ポインターの値を検証して、どこからも参照されていないストレージを探しだすというガベージコレクションの機能は、相性が悪い。
そのため、C++11では、どういうポインターの操作は安全なのかということについて、色々と定義している。その詳細は、本書では解説しない。
サブオブジェクトのストレージの有効期間は、その完全なオブジェクトの有効期間に同じ。
オブジェクトの寿命(lifetime)の始まりは、オブジェクトの型のサイズとアライメントに適切に対応したストレージが確保された後、もしオブジェクトが非トリビアル初期化を持つならば、その初期化が終わった時点である。
非トリビアル初期化(non-trivial initialization)とは、オブジェクトの型がクラスかアグリゲートで、そのメンバーがひとつでもトリビアルデフォルトコンストラクター以外で初期化されるものをいう。
オブジェクトの寿命の終わりは、オブジェクトが非トリビアルデストラクターを持つのならば、デストラクター呼び出しを開始した時点、そうでなければ、オブジェクトの占めるストレージが解放されるか再利用された時点である。
トリビアルにコピー可能な型のオブジェクトは、その内部表現のバイト列を、charかunsigned charの配列にコピーできる。コピーされたcharかunsigned charの配列を、再びオブジェクトにコピーしなおした場合、オブジェクトは元の値を保持する。
// トリビアルにコピー可能な型Tのオブジェクト
T object ;
// Tを表現するバイト列のサイズ
constexpr std::size_t size = sizeof(T) ;
// T型と同じサイズの配列
unsigned char buffer[size] ;
// 配列にコピー
std::memcpy( buffer, &object, size ) ;
// 元のオブジェクトにコピーしなおす
std::memcpy( &object, buffer, size ) ;
// objectの値は元のまま
トリビアルにコピー可能な型Tのオブジェクトを指し示すポインターが二つあるとして、オブジェクトは基本クラスとしてのサブオブジェクトではない場合、内部表現のバイト列をポインターを経由してコピーすると、コピーされた方はコピーした方の値になる。
// Tはトリビアルにコピー可能な型
// ポインターはオブジェクトを指し示しているとする
T * ptr1 ;
T * ptr2 ;
std::memcpy( ptr1, ptr2, sizeof(T) ) ;
// ptr1の指し示すオブジェクトは、ptr2の指し示すオブジェクトと同じ値になる
ある型Tのオブジェクトのオブジェクト表現(object representation)は、Nをsizeof(T)とすると、N個のunsigned char型のオブジェクトになる。オブジェクトの値表現(value representation)は、T型の値を保持するためのビット列である。トリビアルにコピー可能な型の場合、値表現は、オブジェクト表現のビット列の値とすることができる。この値は実装依存のビット列の値である。
宣言されているが定義されていないクラス、一部のenum、大きさの分からない配列や不完全な要素型の配列は、不完全に定義されたオブジェクト型である。このような不完全に定義されたオブジェクト型は、そのサイズやストレージ上のレイアウトが、まだ定まっていない。オブジェクトは、不完全な型を持ったまま定義されてはならない。
struct incomplete ; // 不完全型
struct error
{
incomplete i ; // エラー、不完全な型をもったまま定義
} ;
クラス型は、翻訳単位のある箇所では不完全で、後に完全になることができる。その場合でも、クラスの型は変わらない。
配列の型は、要素として不完全なクラス型を含み、不完全となることがある。もし、クラスが後に完全になったならば、そのような配列型も、その時点で完全になる。
class X ; // 不完全型
using type = X [10] ; // OK、不完全型
type a ; // エラー、配列の要素型が不完全
// クラスXを完全にする
class X { } ;
type b ; // OK、配列の要素型は完全
宣言された配列は要素数を不定とすることができ、その時点では不完全な型となる。そのような配列は、後に完全に定義することができる。配列の型は、要素数が指定される前と後とで異なる。要素の型をTとすると、要素数が指定される前の型は、「T型の要素数不定の配列型」であり、要素数がNに定まった後は、「T型のN要素数の配列」となる。
extern int a[] ; // int型の要素数不定の配列型
int a[10] ; // int型の要素数10の配列型
オブジェクト型(object type)という用語は、CV修飾されているかもしれない型で、関数型、リファレンス型、void型以外の型のことである。
演算型(Arithmetic type)、enum型、ポインター型、メンバーへのポインター型、std::nullptr_t型、そしてこれらの型のCV修飾された型をひっくるめて、スカラー型(scalar type)と呼ぶ。スカラー型とPODクラス、またそのような型の配列と、そのような型がCV修飾された型をひっくるめて、POD型と呼ぶ。スカラー型とトリビアルにコピー可能なクラス型、そのような型の配列、非volatileでconst修飾されたそれらの型をひっくるめて、トリビアルにコピー可能な型(trivially copyable type)と呼ぶ。スカラー型、トリビアルクラス型、そのような型の配列、そのような型のCV修飾された型をひっくるめて、トリビアル型(trivial type)と呼ぶ。スカラー型、標準レイアウトクラス型、そのような型の配列、そのような型のCV修飾された型をひっくるめて、標準レイアウト型(standard layout type)と呼ぶ。
リテラル型(literal type)とは、以下の条件のいずれかひとつを満たす型のことである。
-
void
-
スカラー型
-
リファレンス型
-
リテラル型の配列
-
クラス型で、以下の条件をすべて満たすもの
-
トリビアルデストラクターを持つ
-
アグリゲート型か、少なくとも一つのconstexprコンストラクターか、constexprコンストラクターテンプレートを持ち、そのコンストラクターとコンストラクターテンプレートが、コピーやムーブのコンストラクターではない
-
非staticデータメンバーと基本クラスはすべて、非volatileなリテラル型である
T1型とT2型が同じであるならば、T1とT2は、レイアウト互換型(layout-compatible type)である。
文字(char)のオブジェクトは、基本文字セットをすべて表現できる大きさを持つ。基本文字セットの文字が文字オブジェクトに格納されている場合、文字オブジェクトの整数の値と、その文字の文字リテラルの値は、等しくなる。
char c = 'A' ;
c == 'A' ; // true
char型が負数を表現できるかどうかは、実装依存である。
char c = static_cast<char>(-1) ; // 実装依存
文字型は、明示的にunsignedかsignedで宣言できる。char, signed char, unsigned charは、それぞれ別の型として区別される。この3つの型を、ナロー文字型(狭い文字型、narrow character type)と呼ぶ。
ひとつのchar, signed char, unsigned char型のオブジェクトは同じ大きさのストレージを占め、アライメント要求も同じである。つまり、3つの型が同じオブジェクト表現を持つ。
ナロー文字型では、オブジェクト表現を構成するすべてのビット列が、値表現として使われる。符号なしなナロー文字型では、オブジェクト表現のすべてのビット列が、値表現の数値を表現するのに使われる。この点で、ナロー文字型は、他の型にはない独自の特徴を持つ。慣習的にナロー文字型は任意のバイト列を表現するのに使われている。
char型のオブジェクトの値は、signed charかunsigned charのどちらかの値と等しい。どちらと等しくなるのかは、実装に委ねられている。
char c = 'A' ;
signed char sc = 'A' ;
unsigned char uc = 'A' ;
この例では、規格準拠の実装では、cの値は、scかucのどちらかと必ず等しくなる。どちらと等しいのかは規定されていない。
標準符号つき整数型(standard signed integer type)には、5種類ある。
signed char
short int
int
long int
long long int
この並び順は、小さい順である。標準符号つき整数型は、少なくともこの順序における、前の型の値と同じかそれ以上の大きさのストレージを占める。
基本型(Fundamental types)
この他に、実装依存の拡張符号つき整数型(extended signed integer type)がある。これは、具体的には規格で定義されないが、実装が独自に提供する符号付きの整数型を指す。標準符号つき整数型と、拡張符号つき整数型をひっくるめて、符号つき整数型(signed integer type)と呼ぶ。
素のintは、実行環境のアーキテクチャにとって自然なサイズとなる。
それぞれの標準符号つき整数型に対応する、標準符号なし整数型(standard unsigned integer type)が存在する。
unsigned char
unsigned short int
unsigned int
unsigned long int
unsigned long long int
それぞれ、対応する標準符号つき整数型と、同じ大きさのストレージ、同じアライメント要求を持つ。つまり、符号つきの整数型と対応する符号なしの整数型は、それぞれおなじオブジェクト表現を持つ。
sizeof( int ) == sizeof( unsigned int ) ; // true
alignof( int ) == alignof( unsigned int ) ; // true
標準符号つき整数型と同じように、標準符号なし整数型にも、実装依存の拡張符号なし整数型(extended unsigned integer type)が存在する。これも、それぞれ対応する拡張符号つき整数型とおなじ大きさのストレージとアライメント要求を持つ。
標準符号なし整数型と拡張符号なし整数型をひっくるめて、符号なし整数型(unsigned integer type)と呼ぶ。
符号つき整数型と対応する符号なし整数の型の、値表現は同じである。
標準符号つき整数型と標準符号なし整数型をひっくるめて、標準整数型(standard integer type)と呼ぶ。拡張符号つき整数型と拡張符号なし整数型をひっくるめて、拡張整数型(extended integer type)と呼ぶ。
C++の整数型は、C言語の標準規格で定義されている要件と同じ要件を満たす。つまり、CとC++はこの点において互換性がある。
符号なし整数は、モジュロの2nの法(laws of arithmetic modulo 2n)に従う。nは整数型の値表現のビット数である。これは、符号なし整数型は、絶対にオーバーフローしないことが規格上保証されていることを意味する。何故ならば、もしある符号なし整数型で表現できる値を超えたとしたならば、その結果は、表現できる最大値での剰余になるからだ。
例えば、規格準拠のC++実装では、以下のコードのnとmの値は、保証されている。
int main()
{
// nの値は、unsigned intで表現できる最大値
unsigned int max = std::numeric_limits<unsigned int>::max() ;
unsigned int n = max + 1 ; // nの値は0
unsigned int m = max + 2 ; // mの値は1
}
wchar_tは、内部型(underlying type)と呼ばれる、何らかの整数型と同じサイズ、符号、アライメント要求を持つ。この内部型は、実装に委ねられている。
wchar_t型のひとつのオブジェクトは、実装がサポートするロケールの文字セットの任意の一文字を表現できる。
wchar_tは、少なくとも、規格上はそうなっている。しかし、現実的には、そのような固定長の文字コードは、サポートするロケールと文字セットを大幅に限定しなければ、存在しない。たとえば、ある実装では、wchar_tは16bitのUTF-16の1単位を表現するようになっている。しかし、UTF-16の1単位は、Unicodeの文字セットの任意の一文字を表現できない。ある実装では32bitのUTF-32の1単位を表現するようになっているが、UTF-32も、1単位で任意の1文字を表せる文字のエンコード方式ではない。wchar_tの内部型が規格で規定されていないことにより、wchar_tの仕様は、移植性の問題を引き起こす。
char16_t型は内部型uint_least16_t型と、char32_t型は内部型uint_least32_t型と、それぞれ等しい大きさ、符号、アライメント要求を持つ。
bool型の取り得る値は、trueかfalseである。bool型には、signed, unsigned short, longといった変種は存在しない。
bool, char, char16_t, char32_t, wchar_t、符号付き整数型と符号なし整数型をひっくるめて、整数型(integral typeもしくはinteger type)と呼ぶ。
整数型の内部表現は、何らかの純粋な二進数であると規定されている。規格は、整数型の内部表現について実装依存を認めている。例えば、現実の例をだすと、2の補数だとか1の補数だとか、符号の表現方法だとか、エンディアンなど、整数をビット列で表現するには、様々な実装方法が考えられる。C++の規格は、その詳細を規定しない。
浮動小数点数型(floating point type)には、float, double, long doubleがある。doubleは少なくともfloatと同等以上の精度があり、long doubleはdoubleと同等以上の精度がある。
浮動小数点数型が値を表現する方法は、実装依存である。整数型と浮動小数点数型をひっくるめて、演算型(arithmetic type)と呼ぶ。
void型は、空の値を持つ。値を表現しない型のようなものだ。void型は常に不完全な型であり、完全にする方法はない。
void型は、関数が戻り値を返さない場合に、戻り値の型として使われる。
// 戻り値を返さない関数
void f() { }
あらゆる式は、明示的にvoid型か、CV修飾子つきのvoid型に型変換できる。
// int型の値0をvoid型の空の値に変換
static_cast<void>(0) ;
void型の式が使える場所は、以下の通り。
-
式文
-
コンマ式のオペランド
-
条件演算子の2つ目と3つ目のオペランド
-
typeidのオペランド
-
noexcept
-
decltype
-
戻り値の型がvoidの関数の本体のreturn文の中の式
-
void型、CV修飾子つきのvoid型への型変換のオペランド
#include <typeinfo>
// 呼び出すとvoid型の式になる関数
void f() { }
void g()
{
f() ; // 式文
f() , f() ; // コンマ式のオペランド
true ? f() : f() ; // 条件演算子
typeid( f() ) ; // typeid
noexcept( f() ) ; // noexcept演算子
using type = decltype( f() ) ; // decltype
return f() ; // return文
static_cast<void>( f() ) ; // void型への型変換
}
std::nullptr_t型の値は、nullポインター定数である。nullポインター定数には、nullptrという特別なキーワードのリテラルがある。sizeof(std::nullptr_t)はsizeof(void*)と等しくなる。
たとえ、あるC++の実装で、これらの異なる型として認識される基本型の内部表現が同じだったとしても、型は異なるものと認識される。
複合型とは、ポインターや配列やポインターの配列のように、複数の型が組み合わさって成り立っている型のことである。
複合型は以下の通り。
-
配列
-
関数
-
ポインター
-
リファレンス
-
クラス
-
union
-
enum
-
非staticなクラスのメンバーへのポインター
これらの複合型は、再帰的に適用できる。例えば、ある型へのポインター、ある型へのポインターへのポインター、ある型へのポインターへのポインターの配列、などといったように。構築された型のオブジェクトのバイト数が、std::size_tで表現可能な範囲を超える場合は、エラーとなる。
voidへのポインターと、オブジェクト型へのポインターをひっくるめて、オブジェクトポインター型(object pointer type)と呼ぶ。
ある型Tのオブジェクトへのポインターのことを、「T型へのポインター」という。C++の規格の文面で単に「ポインター」という場合、メンバーへのポインターは含まない。ただし、staticメンバーへのポインターは、メンバーへのポインターではなく、ポインターに含まれることに注意。
不完全な型へのポインターは使えるが、そのポインターを使ってできることは制限される。
オブジェクトポインター型の有効な値は、メモリー上のある1バイトへのアドレスを表現しているか、nullポインターである。たとえば、T型のオブジェクトが、アドレスAに配置されていて、アドレスAの値となるポインターがあった場合、そのポインターは、オブジェクトを指し示している(ポイントしている)と呼ばれる。
ポインター型の値の表現方法は実装依存である。レイアウト互換な型へのCV修飾されたポインターとCV修飾されていないポインターは、同じ値表現と同じアライメント要求を持つ。
CV修飾されているか、CV修飾されていない、void型へのポインターは、型の分からないオブジェクトを指し示すのに使うことができる。void型へのポインターは特別な扱いになっていて、どのようなオブジェクトポインターをも保持できると規定されている。cv void *型のオブジェクトは、cv char *と同じ値の表現とアライメント要求を持つ。
複合型(Compound types)と基本型(Fundamental types)で説明されている型は、CV非修飾型(cv-unqualified type)である。CV非修飾な完全型、不完全型、voidには、三種類のCV修飾された型がある。const修飾された型、volatile修飾された型、const-volatile修飾された型だ。
-
constオブジェクトとは、オブジェクトの型が、const Tとなるか、あるいはそのようなオブジェクトのサブオブジェクトである。
-
volatileオブジェクトとは、オブジェクトの型が、volatile Tとなるか、あるいはそのようなオブジェクトのサブオブジェクトである。
-
const volatileオブジェクトとは、オブジェクトの型が、const volatile Tとなるか、あるいはそのようなオブジェクトのサブオブジェクトである。
ある型のCV修飾された型と、CV非修飾の型は、異なる型である。ただし、同じ表現とアライメント要求を持つ。
CV修飾子には、半順序が存在する。これは、よりCV修飾されている型を比較して決定できる。
その順序は以下の通り。
CV非修飾 < const
CV非修飾 < volatile
CV非修飾 < const volatile
const < const volatile
volatile < const volatile
lvalueとrvalueという用語は、C++の祖先であるC言語のそのまた祖先である、BCPLの頃から、慣習的に使われていた用語である。その本来の意味は、代入式の左右のオペランドに記述することができる値という意味であった。lvalueは代入式の左(left)に書くことができる値(value)であるからして、left valueであり、rvalueは右(right)に書けるのでright valueということだ。
int x ;
x = 0 ;
この例で、xは代入式の左辺に書けるのでlvalueであり、0は右辺に書けるのでrvalueである。
今日では、lvalueとrvalueは、その本来の意味を失い、全く別の意味で使われるようになっている。式の値を分類する用語として使われている。
C++11では、式は、glvalueとrvalueの二種類に分けることができる。これは更に細分化でき、値は三種類の値に分類することができる。lvalueとxvalueとprvalueである。glvalueはlvalueとxvalueに細分化できる。rvalueはprvalueとxvalueに細分化できる。
分類名の意味は、以下のとおりである。
lvalue
lvalueは、関数かオブジェクトである。
lvalueは、名前付きの変数が指し示すオブジェクトや、ポインターを経由して指し示すオブジェクトなどが該当する。
lvalueの名前の由来は、歴史的経緯でleft value(左辺値)であるが、C++では歴史的経緯の意味とは関係がない。
xvalue
xvalueは、オブジェクトである。xvalueのオブジェクトは大抵、その寿命が近いか、あるいは寿命に関心がないことを表現するために使われる。これにより、もしオブジェクトがxvalueであるならば、ムーブしても問題はないということを表現するために使われる。
xvalueは、一部の式の結果や、rvalueリファレンスへの明示的なキャストなどが該当する。
xvalueの名前の由来は、eXpiring value(消失値)である。これは、xvalueというのは寿命が近かったり、寿命に関心がなく、消失しても問題のない値であるという意味から名付けられた。
glvalue
glvalueは、lvalueとxvalueの総称である。
glvalueの名前の由来は、generalized lvalue(一般化lvalue)である。
rvalue
rvalueは、xvalueとprvalueの総称である。xvalueの他には、一時オブジェクトや、リテラルの値(123や3.14やtrueなど)や、特定のオブジェクトに関連付けられていない値などが該当する。
rvalueの名前の由来は、歴史的経緯で、right value(右辺値)であるが、C++では歴史的経緯の意味とは関係がない。
prvalue
prvalue(pure rvalue)は、rvalueのうちxvalueではないものである。これには、一時オブジェクトやリテラルの値や、特定のオブジェクトに関連付けられていない値などが該当する。例えば、関数呼び出しの戻り値で、型がリファレンスではないものもある。
C++03までは、リファレンスは、単に「リファレンス」と呼ばれていた。C++11でいうlvalueリファレンスを意味した。C++03のリファレンスには、rvalueは束縛できなかった。
int f() { return 0 ; }
int main()
{
int & lvalue_ref = f() ; // エラー
}
ただし、constなlvalueリファレンスは、rvalueを束縛できるという例外的なルールがある。
int f() { return 0 ; }
int main()
{
int const & lvalue_ref = f() ; // OK
}
C++にムーブの概念を持ち込むにあたって、rvalueを非constなリファレンスで束縛したいという需要が生まれた。そのため、従来のリファレンスを、lvalueリファレンスとし、新しくrvalueリファレンスを追加することになった。
int f() { return 0 ; }
int main()
{
int && lvalue_ref = f() ; // OK
}
rvalueリファレンスは、rvalueのみを束縛できるリファレンスである。rvalueである以上、寿命がすぐに尽きるか、あるいは、プログラマーはそのオブジェクトの寿命に関心を持たないと明示的に意思表示したとみなすことができる。
そのため、rvalueリファレンスで束縛できたということは、その値の保持する所有権を横取りしても問題がないということになる。
class owner
{
private :
int * ptr ;
public :
owner( int value )
: ptr( new int( value ) )
{ }
// コピーコンストラクター
owner( owner const & lref )
: ptr( new int( *lref.ptr ) )
{ }
// ムーブコンストラクター
owner( owner && rref )
: ptr ( rref.ptr )
{
rref.ptr = nullptr ;
}
~owner( )
{
delete ptr ;
}
} ;
owner f()
{
return owner(123) ;
}
int main()
{
owner o = f() ;
}
rvalueリファレンスの導入により、ストレージなどの確保、解放が必要なリソースや、あるいはファイルやスレッドなどのコピーという概念が存在しないリソースの所有権を、ムーブ(移動)することが可能になった。
このコピーと対をなすムーブという新しい概念は、ムーブセマンティクス(Move Semantics)と呼ばれるプログラミング技法として知られている。プログラミング技法は本書の範疇ではないので、詳しくは解説しない。
オブジェクト型には、アライメント要求(alignment requirements)というものが存在する。これは、オブジェクトが構築されるストレージのアドレスに対する制約である。
アライメント(alignment)とは、メモリ上で連続したオブジェクトを構築するときのアドレスの値に対する、実装依存の整数値である。
アライメント指定子を使うことによって、より厳格なアライメントを要求することができる。詳細はアライメント指定子を参照。
// アライメント8を要求
alignas( 8 ) char[64] ;
alignof式を使うことによって、型のアライメント要求を得ることができる。詳細はalignof式を参照。
// int型のアライメント要求を取得
constexpr std::size_t align_of_int = alignof( int ) ;
以下の例は、連続したメモリ上に確保された二つのint型のオブジェクトの先頭アドレスを表示している。結果は実装により異なる。
#include <cstdio>
int main()
{
int ai[2] ;
std::printf(
"a[0]: %p\n"
"a[1]: %p",
&ai[0], &ai[1] ) ;
}
基本アライメント(fundamental alignment)とは、実装がどのような文脈でもサポートしているアライメントの最大値であり、その値は、alignof( std::max_align_t )に等しい。
以下の例は、基本アライメントの数値を出力するコードである。結果は実装により異なる。
#include <cstddef>
#include <iostream>
int main()
{
std::cout << "fundamental alignment is "
<< alignof( std::max_align_t )
<< std::endl ;
}
ある型のアライメント要求は、その型が完全なオブジェクトとして使われるか、あるいはサブオブジェクトとして使われるかで、変わる可能性がある。
struct B { long double d ; } ;
struct D : virtual B { char c ; } ;
たとえば、上の例のDを、完全なオブジェクトとして使った場合、サブオブジェクトとしてBを含むので、long doubleのアライメント要求も考慮してアラインされる。しかし、もしDが、別のオブジェクトのサブオブジェクトであり、その別のオブジェクトが、Bをvirtual基本クラスとして持つ場合、
D2 : virtual B, D { char c ; } ;
ある実装では、Bのサブオブジェクトは別のオブジェクトのサブオブジェクトとなるかもしれず、DはBをサブオブジェクトとして持たないかもしれない。そのような場合、サブオブジェクトとしてのDのアライメント要求は、Bのアライメント要求に影響されないかも知れない。
これは実装による。alignofの結果は、オペランドの型が完全なオブジェクトとして使われた場合のアライメント要求を返す。
拡張アライメント(extended alignment)は、alignof( std::max_align_t )よりも大きいアライメントである。拡張アライメントがサポートされるかは実装依存である。また、サポートされたとしても、すべての文脈でサポートされないかもしれない。もし、ある文脈で拡張アライメントがサポートされない場合、実装はそのようなコードをエラーにしなければならない。
#include <cstddef>
void f()
{
// この文脈でこの拡張アライメントがサポートされる場合、OK
// サポートされない場合、エラー
alignas( alignof( std::max_align_t ) * 2 ) char buf[64];
}
拡張アライメントを持つ型のことを、アライン超過型(over-aligned type)という。
// アライン超過型の例
struct
alignas( alignof( std::max_align_t ) * 2 )
S { } ;
// これもアライン超過型
struct S2
{
S s ;
} ;
アライメントは、std::size_t型の値で表現される。妥当なアライメントは、alignof式で返される基本型のアライメントと、実装依存のアライメントである。実装依存のアライメントはサポートされていない可能性もある。アライメントは、必ず、負数ではない2の乗数でなければならない。1, 2, 4, 8, 16のような数値は、実装がサポートしていれば、妥当なアライメントである。3, 5, 6, 7, 9のような数値は、妥当なアライメントではない。
アライメントには、順序がある。この順序は、低い方は「より弱い」(weaker)アライメントといい、高い方は「より強い」(stronger)アライメントとか、「より厳格」(stricter)なアライメントという。より厳格なアライメントは、アライメントの数値としての値が高い。あるアライメント要求を満たすアドレスは、そのアライメント要求より弱いアライメント要求も満たす。
完全型のアライメント要求は、alignof式のオペランドに型を与えることで取得できる。
狭い文字型(char, signed char, unsigned char)は、もっとも弱いアライメント要求を持つ。これにより、狭い文字型を、アラインされたメモリ領域のための内部型として使うことができる。
#include <new>
struct S
{
int i ;
double d ;
} ;
void f()
{
// Sを構築するメモリ領域
alignas(S) char buf [ sizeof(S) ] ;
// placement newでSを構築
S * ptr = new( buf ) S ;
// 疑似デストラクター呼び出し
ptr->~S() ;
}
アライメントは比較することができ、その結果は常識的なものである。
-
二つのアライメントの数値が等しい場合、アライメントは等しい
-
二つのアライメントの数値が異なる場合、アライメントは等しくない
-
二つのアライメントのうち、数値の大きいほうが、より厳格なアライメントである
標準ライブラリには、バッファー上で指定されたアライメント要求を満たすアドレスのポインターを返すとか、指定したアライメント要求を満たすアドレスのストレージを確保するライブラリがある。標準ライブラリは本書の範疇ではないので解説しない。
もしある実装で、拡張アライメントがその文脈でサポートされない場合、プログラムはエラーとなる。アライメント要求を指定して動的ストレージを確保する標準ライブラリは、指定されたアライメントに従えない場合、その挙動は確保失敗になる。
標準型変換(Standard conversion)は、暗黙の型変換とも呼ばれている。C++には、多くの組み込み型があるが、異なる型なのにもかかわらず、キャストを使わず、暗黙的に型を変換できる場合がある。この機能のことを、標準型変換という。
short a = 0 ;
int b = a ; // shortからintへ
long c = b ; // intからlongへ
この例では、shortからintへ、intからlongへと、型を変換している。すべての標準型変換が、このように分かりやすくて安全だとは限らない。
int a = 123456789 ;
float b = a ; // intからfloatへ
b = 0.12345 ;
a = b ; // floatからintへ
float型が、int型で表現できる整数の桁をすべて表現できるとは限らない。int型は、整数を表す型であるので、小数点数を正しく表現することはできない。もし、整数と浮動小数点数間で、値を完全に表現できない場合、実装依存の方法で、近い値が使われる。
標準型変換は、人間にとって、できるだけ自然になるように、設計されている。しかし、この標準型変換は、Cから受け継いだ、歴史のある汚い機能なので、どうしても、安全ではない。ここでは、どのような標準型変換があるかを、詳しく説明する。
本書では、普段、「暗黙の型変換」と簡単に呼んでいる標準型変換に、どのようなものがあるのかということを取りあげる。
本書では、煩雑を避けるために省略しているが、多くの標準型変換は、ある型のprvalueの値を、別の型のprvalueの値に変換するようになっている。そのため、標準型変換の際には、必要な場合、glvalueが、自動的にprvalueに変換される。これを、lvalueからrvalueへの型変換という。変換できるglvalueは、関数と配列以外である。
この変換は、通常、まず意識することがない。
配列とポインターは、よく混同される。その理由の一つに、配列名が、あたかもポインターのように振舞うということがある。
int a[10] ;
// pは、aの先頭要素を指す。
int * p = a ;
// どちらも、配列aの先頭要素に0を代入する
*a = 0 ;
*p = 0 ;
これは、配列からポインターへの型変換によるものである。配列名は、配列の先頭要素へのポインターとして扱われる。
int a[10] ;
int * p1 = a ; // &a[0]と同じ
int (* p2 )[10] = &a ; // int [10]へのポインター
ここで、変数aの型は、int [10]であって、int *ではない。ただし、int *に暗黙のうちに型変換されるので、あたかもポインターのように振舞う。
多くの人は、これを暗黙の型変換としては意識していない。配列からポインターへの型変換は、非常によく使われる変換であって、多くの式では、配列名は、自動的に、配列の先頭要素へのポインターに型変換される。
関数の名前は、その関数へのポインターに型変換される。
void f( void ) {}
int main()
{
// typeは関数ポインターの型
using type = void (*) (void) ;
// 同じ意味。
type p1 = f ;
type p2 = &f ;
}
fの型は、関数であって、関数ポインターではない。関数ポインターとは、&fである。しかし、関数は、暗黙のうちに、関数ポインターに型変換されるので、関数名fは、関数ポインターとしても使うことができる。
この型変換も、非常によく使われる。多くの場合は、自動的に、関数は関数ポインターに変換される。
ただし、この型変換は、非staticなメンバー関数には適用されない。ただし、staticなメンバー関数は、この標準変換が適用される。
struct C
{
void f(void) {}
static void g(void) {}
} ;
// エラー
void ( C:: * error )(void) = C::f ;
// OK
void ( C::* ok )(void) = &C::f ;
// staticなメンバー関数は、普通の関数と同じように、変換できる
void (*ptr)(void) = C::g ;
void (*ptr2)(void) = &C::g ; // ただし、こちらの方が分かりやすい
このような暗黙の型変換があるとはいえ、通常、関数ポインターを扱う際には、明示的に単項演算子である&演算子を使ったほうが、分かりやすい。
ある型Tへのポインターは、あるconstまたはvolatile付きの型Tへのポインターに変換できる。
int * p ;
int const * cp = p ;
int volatile * vp = p ;
int const volatile * cvp = p ;
cvp = cp ;
cvp = vp ;
これは、より少ないCV修飾子へのポインターから、より多いCV修飾子へのポインターに、暗黙のうちに型変換できるということである。
ただし、ポインターのポインターの場合は、注意を要する。
int ** p ;
// エラー
int const ** cp = p ;
// これはOK
int const * const * cp = p ;
なぜか。実は、この型変換を認めてしまうと、const性に穴が空いてしまうのだ。
int main()
{
int const x = 0 ;
int * p ;
// これはエラー。
p = &x ;
// もしこれが認められていたとする。
// 実際はエラー。
int const ** cpp = &p ;
// cppを経由して、pを書き換えることができてしまう。
*cpp = &x ;
// pは、xを参照できてしまう。
*p = 0 ;
}
このため、ある型をTとした場合、T **から、T const **への型変換は、認められていない。T **から、T const * const *への変換はできる。
int * p = nullptr ;
int const * const * p2 = &p ; // OK
整数型には、変換順位というものが存在する。これは、標準型変換や、リスト初期化で考慮される、整数型の優先順位である。これは、それほど複雑な順位ではない。基本的には、型のサイズの大小によって決定される。もっとも、多くの場合、型のサイズというのは、実装依存なのだが。
基本的な変換順位は、以下のようになる。
signed char < short int < int < long int < long long int
unsignedな整数型の順位は、対応するsingedな型と同じである。
この他にも、いくつか細かいルールがある。
charとsigned charと、unsigned charは、同じ順位である。
boolは、最も低い順位となる。
char16_t、char32_t、wchar_tの順位は、実装が割り当てる内部的な型に依存する。従って、これらの変換順位は、実装依存である。
拡張整数型、つまり、実装が独自に定義する整数型は、実装依存の順位になる。
整数のプロモーションとは、変換順位の低い型から、高い型へ、型変換することである。ただし、単に順位が低い型から高い型への型変換なら、何でもいいというわけではない。
bool, char16_t, char32_t、wchar_t以外の整数型で、intより変換順位の低い整数型、つまり、char、short、その他の実装独自の拡張整数型は、もし、int型が、その値をすべて表現できる場合、intに変換できる。
short s = 0 ;
int i = s ; // 整数のプロモーション
long l = s ; // これは、整数の型変換
intより低い順位の整数型から、int型への変換ということに注意しなければならない。longやlong longへの変換、または、charからshortへの変換などは、プロモーションではなく、整数の型変換に分類される。
char16_t、char32_t、wchar_tは、実装の都合による内部的な整数型に変換できる。内部的な整数型というのは、int、unsigned int、long int、unsigned long int、long long int、unsigned long long intのいずれかである。もし、これらのどの型でも、すべての値を表現できないならば、実装依存の整数型に変換することができる。
今、int型で、char16_tとchar32_tの取りうるすべての値が表現できるものとすると、
char16_t c16 = u'あ' ;
char32_t c32 = U'あ' ;
wchar_t wc = L'あ' ;
int x = 0 ;
x = c16 ; // xの値は0x3042
x = c32 ; // xの値は0x3042
x = wc ; // xの値は実装依存
int型とwchar_t型のサイズは、実装により異なるので、このコードは、実際のC++の実装では、動く保証はない。
基底型が指定されていないunscoped enum型は、int、unsigned int、long int、unsigned long int、long long int、unsigned long long intのうち、enum型のすべての値を表現できる最初の型に変換できる。もし、どの標準整数型でもすべての値を表現できない場合、すべての値を表現できる実装依存の拡張整数型のうち、もっとも変換順位の低い型が選ばれる。もし、順位の同じ整数型が二つある場合、つまり、signedとunsignedとが違う場合、signedな整数型の方が選ばれる。
基底型が指定されてるunscoped enum型は、指定された基底型に変換できる。その場合で、さらに整数のプロモーションが適用できる場合も、プロモーションとみなされる。例えば、
enum E : short { value } ;
short s = value ; // これは整数のプロモーション
int i = value ; // これも整数のプロモーション
このように、enumの場合は、int型以外への変換でも、プロモーションになる。
int型への代入では、enum型が、基底型であるshortに変換された後、さらにintに変換されている。これは、どちらもプロモーションである。
ビットフィールドは、すべての値を表現できる場合、intに変換できる。
struct A
{
int x:8 ;
} ;
int main()
{
A a = {0} ;
int x = a.x ; // 整数のプロモーション
}
もし、ビットフィールドの値が、intより大きいが、unsigned int型で表現できる場合は、unsigned intに変換できる。値がunsigned intより大きい場合は、整数のプロモーションは行われない。整数の型変換が行われる。
bool型の値は、int型に変換できる。falseは0となり、trueは1となる。
int a = true ; // aは1
int b = false ; // bは0
以上が、整数のプロモーションである。これに当てはまらない整数型同士の型変換は、すべて、次に述べる整数の型変換である。
整数型は、他の整数型に型変換できる。ほんの一例を示すと、
short s = 0 ;
int i = s ; // shortからintへの変換
s = i ; // intからshortへの変換
unsigned int ui = i ; // intからunsigned intへの変換
i = ui ; // unsigned intからintへの変換
long l = s ; // shortからlongへの変換
long long ll = l ; // longからlong longへの変換。
整数のプロモーション以外の整数の型変換は、すべて、整数の型変換になる。この違いは、オーバーロード解決などに影響するので、重要である。
整数の型変換は、危険である。変換先の型が、変換元の値を表現できない場合がある。
例えば、今、signed charは8ビットで、intは16ビットだと仮定する。
#include <limits>
int main()
{
int i = std::numeric_limits<signed char>::max() + 1 ;
signed char c = i ; // どうなる?
}
signed charは、intの取りうる値をすべて表現できるわけではない。この場合、どうなってしまうのか。
変換先の整数型がunsignedの場合、結果の値は、変換元の対応する下位桁の値である。
具体的な例を示して説明する。
// unsigned charが8ビット、unsigned intが16ビットとする
int main()
{
unsigned int ui = 1234 ;
unsigned char uc = ui ; // 210
}
この場合、unsigned int型は、16ビット、uiの値は、2進数で0000010011010010である。unsigned char型は8ビット。つまり、この場合の対応する下位桁の値は、2進数で11010010(uiの下位8ビット)である。よって、ucは、10進数で210となる。
unsignedの場合、変換先の型が、変換元の値を表現できないとしても、その値がどうなるかだけは、保証されている。もっとも、値を完全に保持できないので、危険なことには変わりないのだが。
変換先の整数型がsignedの場合は、非常に危険である。変換先の整数型が、変換元の値を表現できる場合、値は変わらない。表現できない場合、その値は実装依存である。
今仮に、int型は、signed char型の取りうる値をすべて表現できるが、signed char型は、int型の取りうる値をすべて表現することはできないとする。また、signed charは8ビット、intは16ビットとする。signed charの最小値は-127、最大値は127。intの最小値は-32767、最大値は32767とする。
int main()
{
signed char c = 100 ;
int i = c ; // iの値は100
signed char value = 1000 ; // 値は実装依存
}
iの値は、100である。なぜなら、今仮定した環境では、int型は100を表現できるからである。valueの値は、実装依存であり、分からない。なぜならば、signed char型は、1000を表現できないからだ。その場合、変換先のsignedな整数型の値は、実装依存である。
float型の値は、double型の値に変換できる。このとき、値は変わらない。つまり、floatからdoubleへの変換は、まったく同じ値が表現できることを意味している。
float f = 3.14 ;
double d = f ; // dの値は3.14
この変換を、浮動小数点数のプロモーションという。
浮動小数点数のプロモーション以外の、浮動小数点数同士の型変換を、浮動小数点数の型変換という。
double d = 0.0 ;
float f = 0.0 ;
long double ld = 0.0 ;
f = d ; // doubleからfloatへの型変換
ld = f ; // floatからlong doubleへの型変換
ld = d ; // doubleからlong doubleへの型変換
もし、変換先の型が、変換元の型の値を、すべて表現できるのならば、値は変わらない。値を正確に表現できない場合は、最も近い値が選ばれる。この近似値がどのように選ばれるかは、実装依存である。近似値すら表現できない場合の挙動は、未定義である。
浮動小数点数型は、整数型に変換できる。このとき、小数部分は切り捨てられる。小数部分を切り捨てた後の値が、変換先の整数型で表現できない場合、挙動は未定義である。
int x = 1.9 ; // xの値は、1
int y = 1.9999 ; // yの値は、1
int z = 0.9999 ; // zの値は、0
整数型、あるいはunscoped enum型は、浮動小数点数型に変換できる。結果は、可能であれば、まったく同じ値になる。近似値で表現できる場合、実装依存の方法によって、近似値が選ばれる。値を表現できない場合の挙動は、未定義である。
float f = 1 ; // fの値は、1.0f
nullポインター定数とは、整数型定数で、0であるものか、std::nullptr_t型である。
0 ; // nullポインター定数
1 ; // これはnullポインター定数ではない
nullptr ; // nullポインター定数。型はstd::nullptr_t
0がnullポインター定数として扱われるのは、歴史的な理由である。
nullポインター定数は、どんなポインター型にでも変換できる。この値を、nullポインター値(null pointer value)という。nullポインター定数同士を比較すると、等しいと評価される。
int * a = nullptr ;
char * c = nullptr ;
int ** pp = nullptr ;
bool b = (nullptr == nullptr) ; // true
nullポインター定数を、CV修飾付きの型へのポインターに変換する場合、このポインターの型変換のみが行われる。CV修飾子の型変換ではない。
// ポインターの型変換のみが行われる。
// CV修飾子の型変換は行われない。
int const * p = nullptr ;
整数型定数のnullポインター定数は、std::nullptr_t型に変換できる。結果の値は、nullポインターである。
std::nullptr_t null = 0 ;
あるオブジェクトへのポインター型は、voidへのポインターに変換できる。
int x = 0 ;
int * int_pointer = &x ;
void * void_pointer = int_pointer ; // int *からvoid *に変換できる
この時、CV修飾子が付いていた場合、消すことはできない。
int x = 0 ;
int const * int_pointer = &x ;
void * error = int_pointer ; // エラー
void const * ok = int_pointer ; // OK
void *に変換した場合、ポインターの値は、変換元のオブジェクトのストレージの、先頭を指し示す。値がnullポインターの場合は、変換先の型のnullポインターになる。
派生クラスのポインターから、基本クラスのポインターに変換することができる。
struct Base { } ;
struct Derived : Base { } ;
Derived * p = nullptr ;
Base * bp = p ; // OK。Derived *からBase *への変換
もし、基本クラスにアクセス出来ない場合や、曖昧な場合は、エラーとなる。
// 基本クラスにアクセス出来ない場合
struct Base { } ;
struct Derived : private Base { } ;
Derived * d = nullptr ;
Base * b = d ; // エラー。Baseにはアクセス出来ないので、変換できない
// 曖昧な場合
struct Base { } ;
struct Wrap1 : Base { } ;
struct Wrap2 : Base { } ;
// Derivedは、基本クラスとしてふたつのBaseを持っている。
struct Derived : Wrap1, Wrap2 { } ;
Derived * ptr = nullptr ;
// エラー
// Wrap1::Baseと、Wrap2::Baseのどちらなのかが曖昧
Base * ambiguous_base = ptr ;
// OK
// Wrap1::Base
Base * Wrap1_base = static_cast<Wrap1 *>(ptr) ;
派生クラスのポインターから基本クラスポインターへの変換の結果は、派生クラスの中の、基本クラス部分を指す。これは、変換の結果、ポインターの値が変わる可能性がある。実装に依存するので、あまり具体的な例を挙げたくはないが、例えば、以下のようなコードは、多くの実装で、ポインターの値が変わる。
#include <cstdio>
struct Base1 { int x ; } ;
struct Base2 { int x ; } ;
struct Derived : Base1, Base2 { } ;
int main()
{
Derived d ;
// dへのポインター
Derived * d_ptr = &d ;
std::printf("d_ptr : %p\n", d_ptr) ;
// 基本クラスのポインターへ型変換
Base1 * b1_ptr = d_ptr ;
Base2 * b2_ptr = d_ptr ;
// 多くの実装では、
// b1_ptrとb2_ptrのどちらかが、d_ptrと同じ値ではない。
std::printf("b1_ptr : %p\n", b1_ptr) ;
std::printf("b2_ptr : %p\n", b2_ptr) ;
// 派生クラスへキャスト(標準型変換の逆変換)
Derived * d_ptr_from_b1 = static_cast<Derived *>(b1_ptr) ;
Derived * d_ptr_from_b2 = static_cast<Derived *>(b2_ptr) ;
// 多くの実装では、
// d_ptrと同じ値になる。
std::printf("d_ptr_from_b1 : %p\n", d_ptr_from_b1) ;
std::printf("d_ptr_from_b2 : %p\n", d_ptr_from_b1) ;
}
このように、基本クラスと派生クラスの間のポインターのキャストは、ポインターの値の変わる可能性がある。このような型変換には、単に値をそのまま使う、reinterpret_castは使えない。
変換元のポインターの値がnullポインターの場合は、変換先の型のnullポインターになる。
nullポインター定数は、メンバーへのポインターにも変換できる。変換された結果の値を、nullメンバーポインター値(null member pointer value)という。
struct C { int data ; } ;
int C::* ptr = nullptr ;
nullメンバーポインター値は、他のメンバーへのポインターの値と比較できる。
struct C { int data ; } ;
int C::* ptr1 = nullptr ;
int C::* ptr2 = &C::data ;
bool b = ( ptr1 == ptr2 ) ; // false
整数、浮動小数点数、unscoped enum、ポインター、メンバーへのポインターは、boolに変換できる。ゼロ値、nullポインター値、nullメンバーポインター値は、falseに変換される。それ以外の値はすべて、trueに変換される。
bool b1 = 0 ; // false
bool b2 = 1 ; // true
bool b3 = -1 ; // true
bool b4 = nullptr ; // false
int x = 0 ;
bool b5 = &x ; // true
式(expression)とは、演算子(operator)とオペランド(operand)を組み合わせたものである。オペランドとは、言わば、演算子を適用する引数である。式は、何らかの挙動をし、結果を返す。式の結果は、lvalueかxvalueかprvalueになる。
// 演算子は+
// オペランドは1と2
1 + 2 ;
組み込み型以外の型に対しては、演算子はオーバーロードされている可能性がある。その場合の挙動に付いては、オーバーロード関数次第である。
式を評価した際、結果が数学的に定義されていない場合や、型の表現できる範囲を超えた場合の挙動は、未定義である。数学的に定義されていない場合というのは、例えばゼロ除算がある。
多くの二項演算子は、数値型やenumをオペランドに取る。この時、二つのオペランドの型が、それぞれ違う場合、型変換が行われる。この型変換のルールは、以下のようになる。
オペランドにscoped enum型がある場合、変換は行われない。もう片方のオペランドが、同じ型でない場合は、エラーになるからだ。
片方のオペランドが浮動小数点数型の場合、もう片方のオペランドは、その浮動小数点数型に変換される。浮動小数点数型の間の優先順位は、long double > double > floatである。
// オペランドは、long doubleとdouble
// long doubleに変換される
1.0l + 1.0 ;
1.0 + 1.0l ;
// オペランドは、doubleとint
// doubleに変換される
1.0 + 1 ;
1 + 1.0 ;
オペランドが浮動小数点数型ではない場合。つまり、整数型か、unscoped enum型の場合、まず、両方のオペランドに対して、整数のプロモーションが適用される。つまり、int型より変換順位の低い型は、int型に変換される。オペランドがunsignedな整数型の場合は、unsigned intに変換される。その後、両方のオペランドのうち、変換順位が高い方の型に合わせられる。
short s = 0 ;
auto type = s + s ;
この場合、オペランドであるsは、両方とも、int型に変換される。その結果、両方のオペランドは同じ型になるので、結果の型はintになる。
short s = 0 ;
long l = 0 ;
auto type2 = l + s ;
この場合、sはまずint型に変換される。longとintでは、longの方が、変換順位が高いので、結果の型はlongになる。
この時、符号の違う整数型がオペランドになると、非常に複雑な変換が行われるが、本書では解説しない。
式には、優先順位と評価順序がある。
優先順位とは、ある式の中で、異なる式が複数使われた場合、どちらが先に評価されるのかという順位である。この優先順位は、人間にとって自然になるように設計されているので、通常、式の優先順位を気にする必要はない。
// 1 + (2 * 3)
// operator *が優先される
1 + 2 * 3 ;
int x ;
// operator +が優先される
x = 1 + 1 ;
評価順序とは、ある式の中で、優先順位の同じ式が複数使われた場合、どちらを先に評価するかという順序である。これは、式ごとに、「左から右(Left-To-Right)」、あるいは「右から左(Right-To-Left)」のどちらかになる。
たとえば、operator +は、「左から右」である。
// (1 + 1) + 1 と同じ
1 + 1 + 1 ;
一方、operator = は、「右から左」である。
int x ; int y ;
// x = (y = 0) と同じ
x = y = 0 ;
これも、人間にとって自然になるように設計されている。通常、気にする必要はない。
typeid演算子、sizeof演算子、noexcept演算子、decltype型指定子では、あるいは未評価オペランド(unevaluated operand)というものが使われる。このオペランドは文字通り、評価されない式である。
オペランドの式は評価されないが、式を評価した結果の型は、通常の評価される式と何ら変わりない。
int f()
{
std::cout << "hello" << std::endl ;
return 0 ;
}
int main()
{
// int x ; と同等
// 関数fは呼ばれない
decltype( f() ) x ;
}
この例では、オペランドの式の結果の型を、変数xとして宣言、定義している。関数呼び出しの結果の型は、関数の戻り値の型になるので、型はintである。ただし、式自体は評価されないので、実行時に関数が呼ばれることはない。つまり、標準出力にhelloと出力されることはない。
この未評価式は、評価されないということを除けば、通常の式と全く同じように扱われる。例えば、オーバーロード解決やテンプレートのインスタンス化なども、通常の式と同じように働く。
// 関数の宣言だけでいい。定義は必要ない
double f(int) ;
int f(double) ;
int main()
{
// double x ; と同等
decltype( f(0) ) x ;
// int y ; と同等
decltype( f(0.0) ) y ;
}
この例では、関数fは、宣言だけされていて、定義がない。しかし、これは全く問題がない。なぜならば、未評価式は評価されないので、関数fが呼ばれることはない。呼ばれることがなければ、定義も必要はない。
一次式には、多くの細かな種類がある。例えば、リテラルや名前も一次式である。ここでは、一次式の中でも、特に重要なものを説明する。
:: 演算子は、ある名前のスコープを指定する演算子である。このため、非公式に「スコープ解決演算子」とも呼ばれている。しかし、公式の名前は、:: 演算子(operator ::)である。::に続く名前のスコープは、::の前に指定されたスコープになる。
// スコープはグローバル
int x = 0;
// スコープはNS名前空間
namespace NS { int x = 0; }
// スコープはCクラス
struct C { static int x ; } ;
int C::x = 0 ;
int main()
{ // スコープはmain()関数のブロック
int x = 0 ;
x ; // ブロック
::x ; // グローバル
NS::x ; // NS名前空間
C::x ; // クラス
}
このように、::に続く名前のスコープを指定することができる。スコープが省略された場合は、グローバルスコープになる。
式の結果は、::に続く名前が、関数か変数の場合はlvalueに、それ以外はprvalueになる。
括弧式とは、括弧である。これは、式を囲むことができる。括弧式の結果は、括弧の中の式とまったく同じになる。これは主に、ひとつの式の中で、評価する順序を指定するような場合に用いられる。あるいは、単にコードを分かりやすく、強調するために使っても構わない。
(0) ; // 括弧式
// 1 + (2 * 3) = 1 + 6 = 7
1 + 2 * 3 ;
// 3 * 3 = 9
(1 + 2 ) * 3
ほとんどの場合、括弧式の有無は、括弧の中の式の結果に影響を与えない。ただし、括弧式の有無によって意味が変わる場合もある。例えば、decltype指定子だ。
ラムダ式(lambda expression)は、関数オブジェクトを簡単に記述するための式である。以下のような文法になる。
[ ラムダキャプチャーopt ] ( 仮引数 ) mutableopt 例外指定opt -> 戻り値の型opt
ラムダ式を、通常の関数のように使う方法を説明する。まず、ラムダ式の構造は、以下のようになっている。
[ /*ラムダキャプチャー*/ ] // ラムダ導入子
( /*仮引数リスト*/ ) // 省略可能
-> void // 戻り値の型、省略可能
{} // 複合文
これを、通常の関数定義と比較してみる。
auto // 関数宣言
func // 関数名
() // 引数リスト
-> void // 戻り値の型
{} // 関数の定義
ラムダ式は、関数オブジェクトである。通常の関数のように、引数もあれば、戻り値もある。もちろん、通常の関数のように、何も引数に取らないこともできるし、戻り値を返さないこともできる。
// 通常の関数
auto f() -> void {}
// ラムダ式
[]() -> void {} ;
ラムダ式を評価した結果は、prvalueの一時オブジェクトになる。この一時オブジェクトを、クロージャーオブジェクト(closure object)と呼ぶ。クロージャーオブジェクトの型は、クロージャー型(closure type)である。クロージャー型はユニークで、名前がない。これは実装依存の型であり、ユーザーは具体的な型を知ることができない。このクロージャーオブジェクトは、関数オブジェクトと同じように振舞う。
auto f = []() ->void {} ;
ラムダ式は関数オブジェクトなので、通常の関数と同じように、operator ()を適用することで呼び出すことができる。
// 通常の関数
auto f() -> void {}
int main()
{
f() ; // 関数の呼び出し
// ラムダ式
auto g = []() -> void {} ;
g() ; // ラムダ式の呼び出し
// ラムダ式を直接呼び出す
[]() -> void {}() ;
}
仮引数リストと、戻り値の型は、省略できる。従って、最小のラムダ式は、以下の通りになる。
[]{}
仮引数リストを省略した場合は、引数を取らないということを意味する。戻り値の型を省略した場合は、ラムダ式の複合文の内容によって、戻り値の型が推測される。複合文が以下の形になっている場合、
{ return 式 ; }
戻り値の型は、式に lvalueからrvalueへの型変換、 配列からポインターへの型変換、関数からポインターへの型変換を適用した結果の型になる。
それ以外の場合は、void型になる。
注意しなければならないことは、戻り値の型を推測させるためには、複合文は必ず、{ return 式 ; }の形でなければならない。つまり、return文ひとつでなければならないということだ。return文以外に、複数の文がある場合、戻り値の型はvoidである。
// エラー、戻り値の型はvoidだが、値を返している
[]
{
int x = 0 ;
return x ;
}() ;
// OK、戻り値の型を、明示的に指定している。
[] -> int
{
int x = 0 ;
return x ;
}() ;
いくつか例を挙げる。
// 戻り値の型はint
auto type1 = []{ return 0 ; }() ;
// 戻り値の型はdouble
auto type2 = []{ return 0.0 ; }() ;
// 戻り値の型はvoid
[]{ }() ;
[]
{
int x = 0 ;
x = 1 ;
}() ;
ラムダ式の引数は、通常の関数と同じように記述できる。
int main()
{
auto f = []( int a, float b ) { return a ; }
f( 123, 3.14f ) ;
}
複合文は、通常の関数の本体と同じように扱われる。
int main()
{
// 通常の関数と同じように文を書ける
auto f =
[] {
int x = 0 ;
++x ;
};
f() ;
auto g = []
{ // もちろん、複数の文を書ける
int x = 0 ;
++x ; ++x ; ++x ;
} ;
g() ;
}
クロージャーオブジェクトは、変数として保持できる。
#include <functional>
int main()
{
// auto指定子を使う方法
auto f = []{} ;
f() ;
// std::functionを使う方法
std::function< void (void) > g = []{} ;
g() ;
}
ラムダ式は、テンプレート引数にも渡せる。
template < typename Func >
void f( Func func )
{
func() ; // 関数オブジェクトを呼び出す
}
int main()
{
f( []{ std::cout << "hello" << std::endl ; } ) ;
}
ラムダ式の使い方の例を示す。例えば、std::vectorの全要素を、標準出力に出力したいとする。
#include <iostream>
#include <vector>
struct Print
{
void operator () ( int value ) const
{ std::cout << value << std::endl ; }
} ;
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 } ;
std::for_each( v.begin(), v.end(), Print() ) ;
}
この例では、本質的にはたった一行のコードを書くのに、わざわざ関数オブジェクトを、どこか別の場所に定義しなければならない。ラムダ式を使えば、その場に書くことができる。
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 } ;
std::for_each( v.begin(), v.end(),
[](int value){ std::cout << value << std::endl ; } ) ;
}
関数内に関数を書くことができるのは、確かに手軽で便利だ。しかし、ラムダ式は、単にその場に関数を書くだけでは終わらない。ラムダ式は、関数のローカル変数をキャプチャーできる。
#include <iostream>
template < typename Func >
void call( Func func )
{
func() ; // helloと表示する
}
int main()
{
std::string str = "hello" ;
// main関数のローカル変数strを、ラムダ式の中で使う
auto f = [=]{ std::cout << str << std::endl ; } ;
f() ;
// もちろん、他の関数に渡せる。
call( f ) ;
}
このように、ラムダ式が定義されている関数のブロックスコープの中のローカル変数を、使うことができる。この機能を、変数のキャプチャーという。
この、ラムダ式で、定義されている場所のローカル変数を使えるというのは、一見、奇妙に思えるかもしれない。しかし実のところ、これは単なるシンタックスシュガーにすぎない。同じことは、従来の関数オブジェクトでも行える。詳しくは後述する。
もちろん、クロージャーオブジェクトがどのように実装されるかは、実装により異なる。しかし基本的に、ラムダ式は、このような関数オブジェクトへの、シンタックスシュガーに過ぎない。
[]の中身を、ラムダキャプチャーという。ラムダキャプチャーの中には、キャプチャーリストを記述できる。変数のキャプチャーをするには、キャプチャーリストに、どのようにキャプチャーをするかを指定しなければならない。変数のキャプチャーには、二種類ある。コピーでキャプチャーするか、リファレンスでキャプチャーするかの違いである。
int main()
{
int x = 0 ;
// コピーキャプチャー
[=] { x ; }
// リファレンスキャプチャー
[&] { x ; }
}
キャプチャーリストに=を記述すると、コピーキャプチャーになる。&を記述すると、リファレンスキャプチャーになる。
コピーキャプチャーの場合、変数はクロージャーオブジェクトのデータメンバーとして、コピーされる。リファレンスキャプチャーの場合は、クロージャーオブジェクトに、変数への参照が保持される。
コピーキャプチャーの場合は、ラムダ式から、その変数を書き換えることができない。
int main()
{
int x = 0 ;
[=]
{ // コピーキャプチャー
int y = x ; // OK、読むことはできる
x = 0 ; // エラー、書き換えることはできない
} ;
[&]
{ // リファレンスキャプチャー
int y = x ; // OK
x = 0 ; // OK
} ;
}
これは、クロージャーオブジェクトのoperator()が、const指定されているためである。ラムダ式にmutableが指定されていた場合、operator()は、const指定されないので、書き換えることができる。
int main()
{
int x = 0 ;
[=]() mutable
{
int y = x ; // OK
x = 0 ; // OK
} ;
}
リファレンスキャプチャーの場合は、変数の寿命に気をつけなければならない。
#include <functional>
int main()
{
std::function< void ( void ) > f ;
std::function< void ( void ) > g ;
{
int x = 0 ;
f = [&]{ x ; } ; // リファレンスキャプチャー
g = [=]{ x ; } ; // コピーキャプチャー
}
f() ; // エラー、xの寿命は、すでに尽きている。
g() ; // OK
}
ローカル変数の寿命は、そのブロックスコープ内である。この例で、fを呼び出すときには、すでに、xの寿命は尽きているので、エラーになる。
ラムダ式がキャプチャーできるのは、ラムダ式が記述されている関数の、最も外側のブロックスコープ内である。
int main()
{ // 関数の最も外側のブロックスコープ
int x ;
{
int y ;
// xもyもキャプチャーできる。
[=]{ x ; y ; } ;
}
}
関数の最も外側のブロックスコープ以外のスコープ、例えばグローバル変数などは、キャプチャーせずにアクセス出来る。
// グローバルスコープの変数
int x = 0 ;
int main()
{
// キャプチャーする必要はない
[]{ x ; } ;
}
変数ごとに、キャプチャー方法を指定できる。
int main()
{
int a = 0 ;
int b = 0 ;
[ a, &b ]{} ;
}
変数のキャプチャー方法を、それぞれ指定する場合、キャプチャーリストの中に、変数名を記述する。その時、単に変数名だけを記述した場合、コピーキャプチャーになり、変数名の前に&をつけた場合、リファレンスキャプチャーになる。
キャプチャーしたい変数がたくさんある場合、いちいち名前をすべて記述するのは面倒であるので、デフォルトのキャプチャー方法を指定できる。これをデフォルトキャプチャー(default capture)という。この時、デフォルトキャプチャーに続けて、個々の変数名のキャプチャー方法を指定できる。
int main()
{
int a = 0 ; int b = 0 ; int c = 0 ; int d = 0 ;
// デフォルトはコピーキャプチャー
[=]{ a ; b ; c ; d ; } ;
// デフォルトはリファレンスキャプチャー
[&]{ a ; b ; c ; d ; } ;
// aのみリファレンスキャプチャー
[=, &a]{} ;
// aのみコピーキャプチャー
[&, a]{} ;
// a, bのみリファレンスキャプチャー
[=, &a, &b]{} ;
// デフォルトキャプチャーを使わない
[a]{} ;
}
このとき、デフォルトキャプチャーと同じキャプチャー方法を、個々のキャプチャーで指定することはできない。
int main()
{
int a = 0 ; int b = 0 ;
// エラー、デフォルトキャプチャーと同じ
[=, a]{} ;
// OK
[=, &a]{} ;
// エラー、デフォルトキャプチャーと同じ
[&, &a]{} ;
// OK
[&, a]{} ;
}
キャプチャーリスト内で、同じ名前を複数書くことはできない。
int main()
{
int x = 0 ;
// エラー
[x, x]{} ;
}
たとえ、キャプチャー方法が同じであったとしても、エラーになる。
デフォルトキャプチャーが指定されているラムダ式の関数の本体で、キャプチャーできる変数を使った場合、その変数は、暗黙的にキャプチャーされる。
変数のキャプチャーの具体的な使用例を示す。今、vectorの各要素の合計を求めるプログラムを書くとする。関数オブジェクトで実装をすると、以下のようになる。
struct Sum
{
int sum ;
Sum() : sum(0) { }
void operator ()( int value ) { sum += value ; }
} ;
int main()
{
std::vector<int> v = {1,2,3,4,5} ;
Sum sum = std::for_each( v.begin(), v.end(), Sum() ) ;
std::cout << sum.sum << std::endl ;
}
これは、明らかに分かりにくい。sum += valueという短いコードのために、関数オブジェクトを定義しなければならないし、その取扱も面倒である。このため、多くのプログラマは、STLのアルゴリズムを使うより、自前のループを書きたがる。
int main()
{
std::vector<int> v = {1,2,3,4,5} ;
int sum = 0 ;
for ( auto iter = v.begin() ; iter != v.end() ; ++iter )
{
sum += *iter ;
}
std::cout << sum << std::endl ;
}
しかし、ループを手書きするのは分かりにくいし、間違いの元である。ラムダ式のキャプチャーは、この問題を解決してくれる。
int main()
{
std::vector<int> v = {1,2,3,4,5} ;
int sum = 0 ;
std::for_each( v.begin(), v.end(),
[&]( int value ){ sum += value ; }
) ;
std::cout << sum << std::endl ;
}
これで、コードは分かりやすくなる。また、ループを手書きしないので、間違いも減る。
ラムダ式が評価された結果は、クロージャーオブジェクト(closure object)になる。これは、一種の関数オブジェクトで、その型は、ユニークで無名な実装依存のクラスであるとされている。この型は、非常に限定的にしか使えない。例えば、ラムダ式は、未評価式の中で使うことが出来ない。これは、decltypeやsizeofの中で使うことが出来ないということを意味する。
using type = decltype([]{}) ; // エラー
sizeof([]{}) ; // エラー
// OK
auto f = []{} ;
クロージャーオブジェクトがどのように実装されるかは、実装依存である。しかし、今、説明のために、実装の一例を示す。
int main()
{
int a = 0 ; int b = 0 ;
auto f = [a, &b](){ a ; b ; } ;
f() ;
}
例えば、このようなコードがあったとすると、例えば、以下のように実装できる。
class Closure // 本来、ユーザー側から使える名前は存在しない
{
private :
// aはコピーキャプチャー、bはリファレンスキャプチャー
int a ; int & b ;
public :
Closure(int & a, int & b )
: a(a), b(b) { }
// コピーコンストラクターが暗黙的に定義される
Closure( Closure const & ) = default ;
// ムーブコンストラクターが暗黙的に定義される可能性がある
Closure( Closure && ) = default ;
// デフォルトコンストラクターはdelete定義される
Closure() = delete ;
// コピー代入演算子はdelete定義される
Closure & operator = ( Closure const & ) = delete ;
inline void operator () ( void ) const
{ a ; b ; }
} ;
int main()
{
int a = 0 ; int b = 0 ;
auto f = Closure(a, b) ;
f() ;
}
クロージャーオブジェクトは、メンバー関数として、operator ()を持つ。これにより、関数呼び出しの演算子で、関数のように呼び出すことができる。キャプチャーした変数は、データメンバーとして持つ。このoperator ()は、inlineである。また、mutable指定されていない場合、const指定されている。これにより、コピーキャプチャーした変数は、書き換えることができない。mutableが指定されている場合、constではなくなるので、書き換えることができる。
int main()
{
int x = 0 ;
// エラー
[x]() { x = 0 ; } ;
// OK
[x]() mutable { x = 0 ; } ;
}
ラムダ式の仮引数リストには、デフォルト引数を指定できない。
// エラー
[](int x = 0){} ;
ラムダ式は、例外指定できる。
[]() noexcept {} ;
ラムダ式に例外指定をすると、クロージャーオブジェクトのoperator ()に、同じ例外指定がなされたものと解釈される。
クロージャーオブジェクトには、コピーコンストラクターが暗黙的に定義される。ムーブコンストラクターは、可能な場合、暗黙的に定義される。デフォルトコンストラクターと、コピー代入演算子は、delete定義される。これはつまり、初期化はできるが、コピー代入はできないということを意味する。
// 初期化はできる。
auto f = []{} ;
// OK fはラムダ式ではないので可能
using closure_type decltype(f) ;
// OK 初期化はできる
closure_type g = f ;
// エラー、デフォルトコンストラクターは存在しない
closure_type h ;
// エラー、コピー代入演算子は存在しない。
h = f ;
関数ポインターへの変換
ラムダキャプチャーを使わないラムダ式のクロージャーオブジェクトは、同一の引数と戻り値の関数ポインターへの変換関数を持つ。
void (*ptr1)(void) = []{} ;
auto (*ptr2)(int, int, int) -> int = [](int a, int b, int c) -> int { return a + b + c ; };
// 呼び出す。
ptr1() ; ptr2(1, 2, 3) ;
ラムダキャプチャーを使っているクロージャーオブジェクトは、関数ポインターに変換できない。
int main()
{
int x = 0 ;
// エラー、変換できない
auto (*ptr1)(void) -> int = [=] -> int{ return x ; } ;
auto (*ptr2)(void) -> int = [&] -> int{ return x ; } ;
}
変数をキャプチャーしないラムダ式というのは、関数オブジェクトではなく、単なる関数に置き換えることができるので、このような機能が提供されている。この機能は、まだテンプレートを使っていない既存のコードやライブラリとの相互利用のために用意されている。
ラムダ式のネスト
ラムダ式はネストできる。
[]{ // 外側のラムダ式
[]{} ; // 内側のラムダ式
} ;
この時、問題になるのは、変数のキャプチャーだ。内側のラムダ式は、外側のラムダ式のブロックスコープから見える変数しか、キャプチャーすることはできない。
int main()
{
int a = 0 ; int b = 0 ;
[b]{ // 外側のラムダ式
int c = 0 ;
[=]{ // 内側のラムダ式
a ; // エラー、aはキャプチャーできない。
b ; // OK
c ; // OK
} ;
} ;
}
外側のラムダ式が、デフォルトキャプチャーによって、暗黙的に変数をキャプチャーしている場合は、内側のラムダも、その変数をキャプチャーできる。
int main()
{
int a = 0 ;
[=]{ // 外側のラムダ式
[=]{ // 内側のラムダ式
a ; // OK
} ;
} ;
}
基本的にラムダ式は、そのラムダ式が使われているブロックスコープのローカル変数しかキャプチャーできない。しかし、実は、データメンバーを使うことができる。
struct C
{
int x ;
void f()
{
[=]{ x ; } ; // OK、ただし、これはキャプチャーではないことに注意
}
} ;
このように、非staticなメンバー関数のラムダ式では、データメンバーを使うことができる。しかし、これは、データメンバーをキャプチャーしているわけではない。その証拠に、データメンバーを直接キャプチャーしようとすると、エラーになる。
struct C
{
int x ;
void f()
{
[x]{} ; // エラー、データメンバーはキャプチャーできない
}
} ;
では、どうしてデータメンバーが使えるのか。一体何をキャプチャーしているのか。実は、これはthisをキャプチャーしているのである。ラムダ式は、thisをキャプチャーできる。
struct C
{
int x ;
void f()
{
[this]{ this->x ; } ;
}
} ;
ラムダ式の関数の本体では、thisは、クロージャーオブジェクトへのポインターではなく、ラムダ式が使われている非staticなメンバー関数のthisをキャプチャーしたものと解釈される。thisは、必ずコピーキャプチャーされる。というのも、そもそもthisはポインターなので、リファレンスキャプチャーしても、あまり意味はない。
struct C
{
int x ;
void f()
{
[this]{} ; // OK
[&this]{} ; // エラー、thisはリファレンスキャプチャーできない
}
} ;
ラムダ式にデフォルトキャプチャーが指定されていて、データメンバーが使われている場合、thisは暗黙的にキャプチャーされる。デフォルトキャプチャーがコピーでもリファレンスでも、thisは必ずコピーキャプチャーされる。
struct C
{
int x ;
void f()
{
[=]{ x ; } ; // thisをコピーキャプチャーする
[&]{ x ; } ; // thisをコピーキャプチャーする
}
} ;
thisのキャプチャーは、注意を要する。すでに述べたように、データメンバーは、キャプチャーできない。ラムダ式でデータメンバーを使うということは、thisをキャプチャーするということである。データメンバーは、thisを通して使われる。これは、データメンバーは参照で使われるということを意味する。ということは、もし、クロージャーオブジェクトのoperator ()が呼ばれた際に、thisを指し示すオブジェクトが無効になっていた場合、エラーとなってしまう。
struct C
{
int x ;
std::function< int (void) > f()
{
return [this]{ return x ; } ;
}
} ;
int main()
{
std::function< int (void) > f ;
{
C c ;
f = c.f() ;
}// cの寿命はすでに終わっている
f() ; // エラー
}
データメンバーをコピーキャプチャーする方法はない。そもそも、何度も述べているように、データメンバーはキャプチャーできない。では、上の例で、どうしてもデータメンバーの値を使いたい場合はどうすればいいのか。この場合、一度ローカル変数にコピーするという方法がある。
struct C
{
int x ;
std::function< int (void) > f()
{
int x = this->x ;
return [x]{ return x ; } ; // xはローカル変数のコピー
}
} ;
もちろん、同じ名前にするのが紛らわしければ、名前を変えてもいい。
ラムダ式でデータメンバーを使う際には、キャプチャーしているのは、実はthisなのだということに注意しなければならない。
可変引数テンプレートの関数パラメーターパックも、キャプチャーリストに書くことができる。その場合、通常と同じように、パック展開になる。
template < typename ... Types > void g( Types ... args ) ;
template < typename ... Types >
void f( Types ... args )
{
// 明示的なキャプチャー
[args...]{ g( args... ) ; } ;
[&args...]{ g( args... ) ; } ;
// 暗黙的なキャプチャー
[=]{ g( args... ) ; } ;
[&]{ g( args... ) ; } ;
}
後置式は、主にオペランドの後ろに演算子を書くことから、そう呼ばれている。後置式の評価順序はすべて、「左から右」である。
式 [ 式 ]
式 [ 初期化リスト ]
operator []は、添字と呼ばれる式である。これは、配列の要素にアクセスするために用いられる。どちらか片方の式は、Tへのポインター型でなければならず、もう片方は、unscoped enumか、整数型でなければならない。式の結果は、lvalueのTとなる。式、E1[E2] は、*((E1)+(E2)) と書くのに等しい。
int x[3] ;
// *(x + 1)と同じ
x[1] ;
この場合、xには、配列からポインターへの型変換が適用されている。
「どちらか片方の式」というのは、文字通り、どちらか片方である。たとえば、x[1]とすべきところを、1[x]としても、同じ意味になる。
int x[3] ;
// どちらも同じ意味。
x[1] ;
1[x] ;
もっとも、通常は、一つめの式をポインター型にして、二つ目の式を整数型にする。ユーザー定義のoperator []では、このようなことはできない。
ユーザー定義のoperator []の場合、[]の中の式に、初期化リストを渡すことができる。これは、どのように使ってもいいいが、例えば以下のように使える。
struct C
{
int data[10][10][10] ;
int & operator []( std::initializer_list<std::size_t> list )
{
if ( list.size() != 3 ) { /* エラー処理 */ }
auto iter = list.begin() ;
std::size_t const i = *iter ; ++iter ;
std::size_t const j = *iter ; ++iter ;
std::size_t const k = *iter ;
return data[i][j][k] ;
}
} ;
int main()
{
C c ;
c[{1, 2, 3}] = 0 ;
}
初期化リストを使えば、オーバーロードされたoperator []に、複数の引数を渡すことができる。
関数呼び出し(function call)の文法は、以下の通り。
式 ( 引数のリスト )
関数呼び出しには、通常の関数呼び出しと、メンバー関数呼び出しがある。
通常の関数を呼び出す場合、式には、関数へのlvalueか、関数へのポインターが使える。
void f( void ) { }
void g( int ) { }
void h( int, int, int ) { }
int main()
{
// 「関数へのlvalue」への関数呼び出し
f( ) ;
g( 0 ) ;
h( 0, 0, 0 ) ;
// 関数への参照
void (&ref)(void) = f ;
// 「関数へのlvalue」への関数呼び出し
ref() ;
// 関数ポインター
void (*ptr)(void) = &f ;
// 関数ポインターへの関数呼び出し
ptr() ;
}
staticなメンバー関数は、通常の関数呼び出しになる。
struct C { static void f(void) {} } ;
int main()
{
void (*ptr)(void) = &C::f ;
ptr() ; // 通常の関数呼び出し
}
メンバー関数を呼び出す場合、式には、関数のメンバーの名前か、メンバー関数へのポインター式が使える。
struct C
{
void f(void) {}
void g(void)
{
// メンバー関数の呼び出し
f() ;
this->f() ;
(*this).f() ;
// メンバー関数へのポインター
void (C::* ptr)(void) = &C::f ;
// 関数呼び出し
(this->*ptr)() ;
}
} ;
関数呼び出し式の結果の型は、式で呼び出した関数の戻り値の型になる。
void f() ;
int g() ;
// 式の結果の型はvoid
f() ;
// 式の結果の型はint
g() ;
関数呼び出しの結果の型は、戻り値の型になる。これはtypeidやsizeofやdecltypeのオペランドの中でも、同様である。
// 関数fの戻り値の型はint
// すなわち、fを関数呼び出しした結果の型はint
int f() { return 0 ; }
int main()
{
// sizeof(int)と同じ
sizeof( f() ) ;
// typeid(int)と同じ
typeid( f() ) ;
// int型の変数xの宣言と定義。
decltype( f() ) x ;
}
関数が呼ばれた際、仮引数は対応する実引数で初期化される。非staticメンバー関数の場合、this仮引数もメンバー関数を呼び出した際のオブジェクトへのポインターで初期化される。
仮引数に対して、具体的な一時オブジェクトが生成されるかどうかは、実装依存である。たとえば、実装は最適化のために、一時オブジェクトの生成を省略するかもしれない。
仮引数が参照の場合をのぞいて、呼ばれた関数の中で仮引数を変更しても、実引数は変更されない。ただし、型がポインターの場合、参照を通して参照先のオブジェクトが変更される可能性がある。
void f( int x, int & ref, int * ptr )
{
x = 1 ; // 実引数は変更されない
ref = 1 ; // 実引数が変更される
*ptr = 1 ; // 実引数は、ポインターの参照を通して変更される
ptr = nullptr ; // 実引数のポインターは変更されない
}
int main()
{
int x = 0 ; // 実引数
int * ptr = &x ;
f( x, x, ptr ) ;
}
実引数の式が、どのような順番で評価されるかは決められていない。ただし、呼び出された関数の本体に入る際には、式はすべて評価されている。
#include <iostream>
int f1(){ std::cout << "f1" << std::endl ; return 0 ; }
int f2(){ std::cout << "f2" << std::endl ; return 0 ; }
int f3(){ std::cout << "f3" << std::endl ; return 0 ; }
void g( int, int, int ){ }
int main( )
{
g( f1(), f2(), f3() ) ; // f1, f2, f3関数呼び出しの順番は分からない
}
この例では、関数f1, f2, f3がどの順番で呼ばれるのかが分からない。したがって、標準出力にどのような順番で文字列が出力されるかも分からない。ただし、関数gの本体に入る際には、f1, f2, f3は、すべて呼び出されている。
関数は、自分自身を呼び出すことができる。これを再帰呼び出しという。
void f()
{
f() ; // 自分自身を呼び出す、無限ループ
}
ただし、main関数だけは特別で、再帰呼び出しをすることができない。
int main()
{
main() ; // エラー
}
型名 ( 式リスト )
型名 初期化リスト
関数形式の明示的型変換(Explicit type conversion (functional notation))とは、関数呼び出しのような文法による、一種のキャストである。
struct S
{
S( int ) { }
S( int, int ) { }
} ;
int main()
{
int() ;
int{} ;
S( 0 ) ;
S( 1, 2 ) ;
}
型名として使えるのは、単純型指定子か、typename指定子である。単純型指定子でなければならないということには、注意しなければならない。たとえば、ポインターやリファレンス、配列などを直接書くことはできない。ただし、typedef名は使える。
int x = 0 ;
// これらはエラー
int *(&x) ;
int &(x) ;
// typedef名は使える
using type = int * ;
type(&x) ;
単純型指定子の中でも、autoとdecltypeは、注意が必要である。まず、autoは使えない。
auto(0) ; // エラー
decltypeは使える。ただし、非常に使いづらいので、使うべきではない。
// int型をint型にキャスト
// int(0) と同じ
decltype(0)(0) ;
たとえば、以下のコードはエラーである。
int x ;
decltype(x)(x) ; // エラー
これは、文法が曖昧だからだ。詳しくは、曖昧解決を参照。何が起こっているかというと、decltype(x)(x)は、キャストではなく、変数の宣言だとみなされている。decltype(x)は、intという型である。
// decltype(x)(x) と同じ
// decltype(x)(x) → int (x) → int x
int x ;
このため、decltypeを関数形式のキャストで使うのは、問題が多い。使うならば、typedef名をつけてから使うか、static_castを使うべきである。
int x ;
using type = decltype(x) ;
type(x) ;
static_cast< decltype(x) >(x) ;
typename指定子も使うことができる。
template < typename T >
void f()
{
typename T::type() ; // OK
}
式リストが、たったひとつの式である場合、キャストと同じ意味になる。
// int型からshort型へのキャスト
short(0) ;
// int型からdouble型へのキャスト
double(0) ;
struct C { C(int) {} } ;
// 変換関数による、int型からC型へのキャスト
C(0) ;
型名がクラス名である場合、T(x1, x2, x3)という形の式は、T t(x1, x2, x3)という形と同じ意味を持つ一時オブジェクトを生成し、その一時オブジェクトを、prvalueの結果として返す。型名がクラス名でも、式リストがひとつしかない場合は、キャストである。もっとも、その場合も、ユーザー定義のコンストラクターが、変換関数として呼び出されることになるので、意味はあまり変わらない。
struct C
{
C(int) {}
C(int, int) {}
C(int, int, int) {}
} ;
int main()
{
C(0) ; // これはキャスト、意味としては、あまり違いはない
C(1, 2) ;
C(1, 2, 3) ;
}
式リストが空の場合、つまり、T()という形の式の場合。まず、Tは配列型であってはならない。Tは完全な型か、voidでなければならない。式の結果は、値初期化された型のprvalueの値になる。値初期化については、初期化子を参照。
int() ; // int型の0で初期化された値
double() ; // double型の0で初期化された値
struct C {} ;
C() ; // デフォルトコンストラクターが呼ばれたCの値
void型の場合、値初期化はされない。式の結果の型はvoidである。
void() ; // 結果はvoid
括弧で囲まれた式リストではなく、初期化リストの場合、式の結果は、指定された型の、初期化リストによって直接リスト初期化されたprvalueの一時オブジェクトになる。
#include <initializer_list>
struct C
{
C( std::initializer_list<int> ) { }
} ;
int main()
{
C{1,2,3} ;
}
疑似デストラクター呼び出しとは、デストラクターを明示的に呼び出すことができる一連の式である。使い方は、operator .、operator ->に続けて、疑似デストラクター名を書き、さらに関数呼び出しのoperator ()を書く。この一連の式を、疑似デストラクター呼び出しという。このような疑似デストラクター名に続けては、関数呼び出し式しか適用することができない。式の結果はvoidになる。
// このコードは、疑似デストラクター呼び出しの文法を示すためだけの例である
struct C {} ;
int main()
{
C c ;
c.~C() ; // 疑似デストラクター呼び出し
C * ptr = &c
ptr->~C() ; // 疑似デストラクター呼び出し
}
注意すべきことは、デストラクターを明示的に呼び出したとしても、暗黙的に呼び出されるデストラクターは、依然として呼び出されるということである。
#include <iostream>
struct C
{
~C() { std::cout << "destructed." << std::endl ; }
} ;
int main()
{
{
C c ;
c.~C() ; // デストラクターを呼び出す
}// ブロックスコープの終りでも、デストラクターは暗黙的に呼ばれる
C * ptr = new C ;
ptr->~C() ; // デストラクターを呼び出す
delete ptr ; // デストラクターが暗黙的に呼ばれる。
}
このように、通常は、デストラクターの呼び出しが重複してしまう。二重にデストラクターを呼び出すのは、大抵の場合、プログラム上のエラーである。では、疑似デストラクター呼び出しは何のためにあるのか。具体的な用途としては、placement newと組み合わせて使うということがある。
struct C { } ;
int main()
{
// placement new用のストレージを確保
void * storage = operator new( sizeof(C) ) ;
// placement new
C * ptr = new(storage) C ;
// デストラクターを呼び出す
ptr->~C() ;
// placement new用のストレージを解放
operator delete( storage ) ;
}
この疑似デストラクターには、decltypeを使うことができる。
struct C {} ;
int main()
{
C c ;
c.~decltype(c) ;
C * ptr = &c
ptr->~decltype(c) ;
}
テンプレート引数の場合、型がスカラー型であっても、疑似デストラクター呼び出しができる。
template < typename T >
void f()
{
T t ;
t.~T() ;
}
int main()
{
f<int>() ;
}
これにより、ジェネリックなテンプレートコードが書きやすくなる。
クラスのオブジェクト . メンバー名
クラスのポインター -> メンバー名
クラスメンバーアクセスは、名前の通り、クラスのオブジェクトか、クラスのオブジェクトへのポインターのメンバーにアクセスするための演算子である。
. 演算子の左側の式は、クラスのオブジェクトでなければならない。-> 演算子の左側の式は、クラスのオブジェクトへのポインターでなければならない。演算子の右側は、そのクラスか、基本クラスのメンバー名でなければならない。-> 演算子を使った式、E1->E2は、(*(E1)).E2という式とおなじになる。
struct Object
{
int x ;
static int y ;
void f() {}
} ;
int Object::y ;
int main()
{
Object obj ;
// . 演算子
obj.x = 0 ;
obj.y = 0 ;
obj.f() ;
Object * ptr = &obj ;
// -> 演算子
ptr->x = 0 ;
ptr->y = 0 ;
ptr->f() ;
}
もし、クラスのオブジェクト、クラスのオブジェクトへのポインターを表す式が依存式であり、メンバー名がメンバーテンプレートであり、テンプレート引数を明示的に指定したい場合、メンバー名の前に、templateキーワードを使わなければならない。
struct Object
{
template < typename T >
void f() {}
} ;
template < typename T >
void f()
{
T obj ;
obj.f<int>() ; // エラー
obj.template f<int>() ; // OK
}
int main()
{
f<Object>() ;
}
これは、<演算子や、>演算子と、文法が曖昧になるためである。この問題については、テンプレート特殊化の名前でも、解説している。
派生によって、クラスのメンバー名が曖昧な場合、エラーになる。
struct Base1 { int x ; } ;
struct Base2 { int x ; } ;
struct Derived : Base1, Base2
{ } ;
int main()
{
Derived d ;
d.x ; // エラー
d.Base1::x ; // OK
d.Base2::x ; // OK
}
ここでは、後置式のインクリメントとデクリメントについて解説する。前置式のインクリメントとデクリメントについては、単項式のインクリメントとデクリメントを参照。
式 ++
式 --
後置式の++演算子の式の結果は、オペランドの式の値になる。オペランドは、変更可能なlvalueでなければならない。オペランドの型は、数値型か、ポインター型でなければならない。式が評価されると、オペランドに1を加算する。ただし、式の結果は、オペランドに1を加算する前の値である。
int x = 0 ;
int result = x++ ;
// ここで、result == 0, x == 1
式の結果の値は、オペランドの値と変わりがないが、オペランドには、1を加算されるということに注意しなければならない。
後置式の--演算子は、オペランドから1を減算する。それ以外は、++演算子と全く同じように動く。
int x = 0 ;
int result = x-- ;
// ここで、result == 0, x == -1
dynamic_cast < 型名 > ( 式 )
dynamic_cast<T>(v)という式は、vという式をTという型に変換する。便宜上、vをdynamic_castのオペランド、Tをdynamic_castの変換先の型とする。変換先の型はクラスへのポインターかリファレンス、あるいは、voidへのポインター型でなければならない。オペランドは、変換先の型が、ポインターの場合はポインター、リファレンスの場合はリファレンスでなければならない。
struct C {} ;
int main()
{
C c ;
// 変換先の型がポインターの場合は、オペランドもポインター
// 変換先の型がリファレンスの場合は、オペランドもリファレンスでなければならない
dynamic_cast<C &>(c) ; // OK
dynamic_cast<C *>(&c) ; // OK
// ポインターかリファレンスかが、一致していない
dynamic_cast<C *>(c) ; // エラー
dynamic_cast<C &>(&c) ; // エラー
}
今、Derivedクラスが、Baseクラスから派生されていたとする。
struct Base {} ;
struct Derived : Base {} ;
この時、static_castを使えば、Baseへのポインターやリファレンスから、Derivedへのポインターやリファレンスに変換することができる。
int main()
{
Derived d ;
Base & base_ref = d ;
Derived & derived_ref = static_cast<Derived &>(base_ref) ;
Base * base_ptr = &d ;
Derived * derived_ptr = static_cast<Derived *>(base_ptr) ;
}
この例では、ポインターやリファレンスが指す、本当のオブジェクトは、Derivedクラスのオブジェクトだということが分かりきっているので安全である。しかし、ポインターやリファレンスを使う場合、常にオブジェクトの本当のクラス型が分かるわけではない。
void f( Base & base )
{
// baseがDerivedを参照しているかどうかは、分からない。
Derived & d = static_cast<Derived &>(base) ;
}
int main()
{
Derived derived ;
f(derived) ; // ok
Base base ;
f(base) ; // エラー
}
このように、ポインターやリファレンスの指し示すオブジェクトの本当のクラス型は、実行時にしか分からない。しかし、オブジェクトの型によって、特別な処理をしたいことも、よくある。
void f( Base & base )
{
if ( /* baseの指すオブジェクトがDerivedクラスの場合*/ )
{
// 特別な処理
}
// 共通の処理
}
本来、このような処理は、virtual関数で行うべきである。しかし、現実には、どうしても、このような泥臭くて汚いコードを書かなければならない場合もある。そのようなどうしようもない場合のために、C++には、基本クラスへのポインターやリファレンスが、実は派生クラスのオブジェクトを参照している場合に限り、キャストできるという機能が提供されている。それが、dynamic_castである。
動的な型チェックを使うためには、dynamic_castのオペランドのクラスは、ポリモーフィック型でなければならない。つまり、少なくともひとつのvirtual関数を持っていなければならない。ポリモーフィック型の詳しい定義については、virtual関数を参照。
もし、オペランドの参照するオブジェクトが、変換先の型として指定されている派生クラスのオブジェクトであった場合、変換することができる。
struct Base { virtual void f() {} } ;
struct Derived : Base {} ;
void f(Base & base)
{ // baseはDerivedを指しているとする
Derived & ref = dynamic_cast<Derived &>(base) ;
Derived * ptr = dynamic_cast<Derived *>(&base) ;
}
実引数に、変換先の型ではないオブジェクトを渡した場合、dynamic_castの変換は失敗する。変換が失敗した場合、変換先の型がリファレンスの場合、std::bad_castがthrowされる。変換先の型がポインターの場合、nullポインターが返される。
struct Base { virtual void f() {} } ;
struct Derived : Base {} ;
int main()
{
Base base ;
// リファレンスの場合
try
{
Derived & ref = dynamic_cast<Derived &>(base) ;
}
catch ( std::bad_cast )
{
// 変換失敗
// リファレンスの場合、std::bad_castがthrowされる
}
// ポインターの場合
Derived * ptr = dynamic_cast<Derived *>(&base) ;
if ( ptr == nullptr )
{
// 変換失敗
// ポインターの場合、nullポインターが返される
}
}
基本クラスのポインターやリファレンスが、実際は何を指しているかは、実行時にしか分からない。そのため、常に変換に失敗する可能性がある。そのため、dynamic_castを使う場合は、常に変換が失敗するかもしれないという前提のもとに、コードを書かなければならない。
失敗せずに変換できる場合というのは、オペランドの指すオブジェクトの本当の型が、変換先の型のオブジェクトである場合で、しかもアクセスできる場合である。オブジェクトである(is a)場合というのは、例えば、
struct A { virtual void f(){} } ;
struct B : A {} ;
struct C : B {} ;
struct D : C {} ;
このようなクラスがあった場合、Dは、Cであり、Bであり、Aである。従って、Dのオブジェクトを、Aへのリファレンスで保持していた場合、D、C、Bのいずれにも変換できる。
int main()
{
D d ;
A & ref = d ;
// OK
// refの指しているオブジェクトは、Dなので、変換できる。
dynamic_cast<D &>(ref) ;
dynamic_cast<C &>(ref) ;
dynamic_cast<B &>(ref) ;
}
アクセスできる場合というのは、変換先の型から、publicで派生している場合である。
struct Base1 { virtual void f(){} } ;
struct Base2 { virtual void g(){} } ;
struct Base3 { virtual void h(){} } ;
struct Derived
: public Base1,
public Base2,
private Base3
{ } ;
int main()
{
Derived d ;
Base1 & ref = d ;
// OK、Base2はpublicなので、アクセス出来る
dynamic_cast<Base2 &>(ref) ;
// 実行時エラー、Base3はprivateなので、アクセス出来ない
// std::bad_castがthrowされる。
dynamic_cast<Base3 &>(ref) ;
}
この例の場合、refが参照するオブジェクトは、Derived型であるので、Base3型のサブオブジェクトも持っているが、Base3からは、privateで派生されているために、アクセスすることはできない。そのため、変換することが出来ず、std::bad_castがthrowされる。
変換先の型は、void型へのポインターとすることもできる。その場合、オペランドの指す本当のオブジェクトの、もっとも派生されたクラスを指すポインターが、voidへのポインター型として、返される。
struct Base { virtual void f(){} } ;
struct Derived1 : Base {} ;
struct Derived2 : Derived1 {} ;
int main()
{
Derived1 d1 ;
Base * d1_ptr = &d1 ;
// Derived1を指すポインターの値が、void *として返される
void * void_ptr1 = dynamic_cast<void *>(d1_ptr) ;
Derived1 d2 ;
Base * d2_ptr = &d2;
// Derived2を指すポインターの値が、void *として返される
void * void_ptr2 = dynamic_cast<void *>(d2_ptr) ;
}
一般に、この機能はあまり使われることがないだろう。
dynamic_castは、その主目的の機能の他にも、クラスへのポインターやリファレンスに限って、キャストを行うことができる。この機能は、標準型変換のポインターの型変換に、ほぼ似ている。このキャストは、static_castでも行える。以下の機能に関しては、実行時のコストは発生しない。
オペランドの型が、変換先の型と同じ場合、式の結果の型は、変換先の型になる。この時、constとvolatileを付け加えることはできるが、消し去ることは出来ない。
// 型と式が同じ場合の例
struct C { } ;
int main()
{
C v ;
dynamic_cast<C &>(v) ;
dynamic_cast<C const &>(v) ; // constを付け加える
C const cv ;
dynamic_cast<C &>(cv) ; // エラー、constを消し去ることは出来ない
// ポインターの場合
C * ptr = &v ;
dynamic_cast<C *>(ptr) ;
dynamic_cast<C const *>(ptr) ;
}
変換先の型が基本クラスへのリファレンスで、オペランドの型が、派生クラスへのリファレンスの場合、dynamic_castの結果は、派生クラスのうちの基本クラスを指すリファレンスになる。ポインターの場合も、同様である。
struct Base {} ; // 基本クラス
struct Derived : Base {} ; // 派生クラス
int main()
{
Derived d ;
Base & base_ref = dynamic_cast<Base &>(d) ;
Base * base_ptr = dynamic_cast<Base *>(&d) ;
}
typeid ( 式 )
typeid ( 型名 )
typeidとは、式や型名の、型情報を得るための式である。型情報は、const std::type_infoのリファレンスという形で返される。std::type_infoについての詳細は、RTTI(Run Time Type Information)を参照。typeidを使うには、必ず、<typeinfo>ヘッダーを#includeしなければならない。ただし、本書のサンプルコードは、紙面の都合上、必要なヘッダーのincludeを省略していることがある。
typeidのオペランドは、sizeofに似ていて、式と型名の両方を取ることができる。
#include <typeinfo>
int main()
{
// 型名の例
typeid(int) ;
typeid( int * ) ;
// 式の例
int x = 0 ;
typeid(x) ;
typeid(&x) ;
typeid( x + x ) ;
}
typeidのオペランドの式が、ポリモーフィッククラス型のglvalueであった場合、実行時チェックが働き、結果のstd::type_infoが表す型情報は、実行時に決定される。型情報は、オブジェクトの最も派生したクラスの型となる。
struct Base { virtual void f() {} } ;
struct Derived : Base {} ;
int main()
{
Derived d ;
Base & ref = d ;
// オブジェクトの、実行時の本当の型を表すtype_infoが返される
std::type_info const & ti = typeid(d) ;
// true
ti == typeid(Derived) ;
// Derivedを表す、人間の読める実装依存の文字列
std::cout << ti.name() << std::endl ;
}
オペランドの式の型がポリモーフィッククラス型のglvalueの場合で、nullポインターを参照した場合は、std::bad_typeidが投げられる。
struct Base { virtual void f() {} } ;
struct Derived : Base {} ;
int main()
{
// ptrの値はnullポインター
Base * ptr = nullptr ;
try
{
typeid( *ptr ) ; // 実行時エラー
}
catch ( std::bad_typeid )
{
// 例外が投げられて、ここに処理が移る
}
}
オペランドの式の型が、ポリモーフィッククラス型でない場合は、std::type_infoが表す型情報は、コンパイル時に決定される。
// int型を表すtype_info
typeid(0) ;
// double型を表すtype_info
typeid(0.0) ;
int f() {}
// int型を表すtype_info
typeid( f() ) ;
この際、lvalueからrvalueへの型変換、配列からポインターへの型変換、関数からポインターへの型変換は行われない。
// 配列からポインターへの型変換は行われない
int a[10] ;
// 型情報は、int [10]
// int *ではない
typeid(a) ;
// 関数からポインターへの型変換は行われない
void f() {}
// 型情報は、void (void)
// void (*)(void)ではない。
typeid(f) ;
これらの標準型変換は、C++では、非常に多くの場所で、暗黙のうちに行われているので、あまり意識しない。たとえば、テンプレートの実引数を推定する上では、これらの変換が行われる。
// 実引数の型を、表示してくれるはずの便利な関数
template < typename T >
void print_type_info(T)
{
std::cout << typeid(T).name() << std::endl ;
}
void f() { }
int main()
{
int a[10] ;
// int [10]
std::cout << typeid(a).name() << std::endl ;
// int *
print_type_info(a) ;
// void (void)
std::cout << typeid(f).name() << std::endl ;
// void (*)(void)
print_type_info(f) ;
}
std::type_info::name()の返す文字列は実装依存だが、今、C++の文法と同じように型を表示すると仮定すると、このような出力になる。C++では、多くの場面で、暗黙のうちに、これら三つの型変換が行われるので、このような差異が生じる。
オペランドが、型名の場合は、std::type_infoは、その型を表す。ほんの一例をあげると。
int main()
{
typeid( int ) ; // int型
typeid( int * ) ; // intへのポインター型
typeid( int & ) ; // intへのlvalueリファレンス型
typeid( int [2] ) ; // 配列型
typeid( int (*)[2] ) ; // 配列へのポインター型
typeid( int (int) ) ; // 関数型
typeid( int (*)(int) ); // 関数へのポインター型
}
オペランドの式や型名の、トップレベル(top-level)のCV修飾子は、無視される。
int main()
{
// トップレベルのCV修飾子は無視される
typeid(const int) ; // int
// 当然、型情報は等しい
typeid(const int) == typeid(int) ; // true
// 型名も式も同じ
int i = 0 ; const int ci = 0;
typeid(ci) ; // int
typeid(ci) == typeid(i) ; // true
// これはトップレベルのCV修飾子
typeid(int * const) ; // int *
// 以下はトップレベルのCV修飾子ではない
typeid(const int *) ; // int const *
typeid(int const *) ; // int const *
}
static_cast< 型名 >( 式 )
static_castは、実に多くの静的な変換ができる。その概要は、標準型変換とその逆変換、ユーザー定義の変換、リファレンスやポインターにおける変換など、比較的安全なキャストである。以下にstatic_castの行える変換を列挙するが、これらを丸暗記しておく必要はない。もし、どのキャストを使うか迷った場合は、static_castを使っておけば、まず間違いはない。static_castがコンパイルエラーとなるキャストは、大抵、実装依存で危険なキャストである。
static_castによるキャストがどのように行われるかは、おおむね、以下のような順序で判定される。条件に合う変換方法が見つかった時点で、それより先に行くことはない。これは、完全なstatic_castの定義ではない。分かりやすさのため省いた挙動もある。
static_cast<T>(v)の結果は、オペランドvを変換先の型Tに変換したものとなる。変換先の型がlvalueリファレンスならば結果はlvalue、rvalueリファレンスならば結果はxvalue、それ以外の結果はprvalueとなる。static_castは、constとvolatileを消し去ることはできない。
オペランドの型が基本クラスで、変換先の型が派生クラスへのリファレンスの場合。もし、標準型変換で、派生クラスのポインターから、基本クラスのポインターへと変換できる場合、キャストできる。
struct Base {} ;
struct Derived : Base {} ;
void f(Base & base)
{
// Derived *からBase *に標準型変換で変換できるので、キャストできる
Derived & derived = static_cast<Derived &>(base) ;
}
ただし、これには実行時チェックがないので、baseが本当にDerivedのオブジェクトを参照していなかった場合、動作は未定義である。
glvalueのオペランドは、rvalueリファレンスに型変換できる。
int main()
{
int x = 0 ;
static_cast<int &&>(x) ;
}
もし、static_cast<T>(e) という式で、T t(e); という宣言ができる場合、オペランドの式eは、変換先の型Tに変換できる。その場合、一時オブジェクトを宣言して、それをそのまま使うのと、同じ意味になる。
// short t(0) は可能なので変換できる
static_cast<short>(0) ;
// float t(0) は可能なので変換できる
static_cast<float>(0) ;
標準型変換の逆変換を行うことができる。ただし、いくつかの変換は、逆変換を行えない。これには、lvalueからrvalueへの型変換、配列からポインターへの型変換、関数からポインターへの型変換(Function-to-pointer conversion)、ポインターや関数ポインターからnullポインターへの変換、がある。
変換先の型に、voidを指定することができる。その場合、static_castの結果はvoidである。オペランドの式は評価される。
int main()
{
static_cast<void>( 0 ) ;
static_cast<void>( 1 + 1 ) ;
}
整数型とscoped enum型は、static_castを使うことで、明示的に変換することができる。その場合、変換先の型で、オペランドの値を表現できる場合は、値が保持される。値を表現できない場合の挙動は、規定されていない。
int main()
{
enum struct A { value = 1 } ;
enum struct B { value = 1 } ;
int x = static_cast<int>( A::value ) ;
A a = static_cast<A>(1) ;
B b = static_cast<B>( A::value ) ;
}
派生クラスへのポインターから、基本クラスへのポインターにキャストできる。
struct Base {} ;
struct Derived : Base {} ;
Derived d ;
Base * ptr = static_cast<Base *>(&d) ;
voidへのポインターは、他の型へのポインターに変換できる。ある型へのポインターから、voidへのポインターにキャストされ、そのまま、ある型へのポインターにキャストされなおされた場合、その値は保持される。
int main()
{
int x = 0 ;
int * ptr = &x ;
// void *への変換は、標準型変換で行えるので、キャストはなくてもよい。
void * void_ptr = static_cast<void *>(ptr) ;
// キャストが必要
ptr = static_cast<int *>(void_ptr) ;
*ptr ; // ポインターの値は保持されるので、xを正しく参照する
}
reinterpret_cast < 型名 > ( 式 )
reinterpret_castは、式の値をそのまま、他の型に変換するキャストである。ポインターと整数の間の変換や、ある型へのポインターを全く別の型へのポインターに変換するといったことができる。reinterpret_castを使えば、値をそのままにして、型を変換することができる。変換した結果、その値が、変換先の型としてそのまま使えるかどうかなどといったことは、ほとんど規定されていない。元の値をそのまま保持できるかどうかも分からない。それ故、reinterpret_castは、危険なキャストである。
多くの実装では、reinterpret_castには、何らかの具体的で実用的な意味がある。現実のC++が必要とされる環境では、reinterpret_castを使わなければならないことも、多くある。しかし、reinterpret_castを使った時点で、そのコードは実装依存であり、具体的に意味が定義されたその環境でしか動かないということを、常に意識するべきである。
reinterpret_castでは、constやvolatileを消し去ることはできない。
reinterpret_castでできる変換を、以下に列挙する。
ポインター型と整数型の間の型変換
ある型へのポインター型から整数型へのキャスト、あるいはその逆に、整数型やenum型からポインター型へのキャストを行える。整数型は、ポインターの値をそのまま保持できるほど大きくなければならない。どの整数型ならば十分に大きいのか。もし整数型が十分に大きくなければどうなるのかなどということは、定義されていない。
int main()
{
int x = 0 ;
int * ptr = &x ;
// ポインターから整数へのキャスト
int value = reinterpret_cast<int>(ptr) ;
// 整数からポインターへのキャスト
ptr = reinterpret_cast<int *>(value) ;
}
これらのキャストについての挙動は、ほとんどが実装依存であり、あまり説明できることはない。
もし、変換先の整数型が、ポインターの値をすべて表現できるとするならば、再びポインター型にキャストし直した時、ポインターは同じ値を保持すると規定されている。しかし、int型がポインターの値をすべて表現できるという保証はない。unsigned intであろうと、long intであろうとlong long intであろうと、そのような保証はない。従って、上記のコードで、ptrが同じ値を保つかどうかは、実装依存である。
異なるポインター型の間の型変換
ある型へのポインターは、まったく別の型へのポインターに変換できる。たとえば、int *からshort *などといった変換ができる。
int main()
{
int x = 0 ;
int * int_ptr = &x ;
// int * からshort *へのキャスト
short * short_ptr = reinterpret_cast<short *>(int_ptr) ;
}
これについても、挙動は実装依存であり、特に説明できることはない。たとえば、上記のコードで、short_ptrを参照した場合どうなるのかということも、全く規定されていない。ある実装では、問題なく、int型のストレージを、あたかもshort型のストレージとして使うことができるかもしれない。ある実装では、参照した瞬間にプログラムがクラッシュするかもしれない。
異なるリファレンス型の間の型変換
異なるポインター型の間の型変換に似ているが、異なるリファレンス型の変換をすることができる。例えば、int &からshort &などといった変換ができる。
int main()
{
int x = 0 ;
// int & からshort &へのキャスト
short & short_ref = reinterpret_cast<short &>(x) ;
}
異なるポインター型の間の型変換と同じで、これについても、具体的な意味は実装依存である。
異なるメンバーポインターの間の型変換
異なるメンバーポインターへ変換することができる。
struct A { int value ; } ;
struct B { int value ; } ;
int main()
{
int B::* ptr = reinterpret_cast<int B::*>(&A::value) ;
}
意味は、実装依存である。
異なる関数ポインター型の間の型変換
ある関数ポインターは、別の型の関数ポインターにキャストできるかもしれない。意味は実装依存である。「かもしれない」というのは実に曖昧な表現だが、たとえ完全に規格準拠な実装であっても、この機能をサポートする義務がないという意味である。
void f(int) {}
int main()
{
// void (short)な関数へのポインター型
using type = void (*)(short) ;
// 関数ポインターの型変換
type ptr = reinterpret_cast<type>(&f) ;
}
この変換がどういう意味を持つのか。例えば、変換した結果の関数ポインターは、関数呼び出しできるのか。できるとして、一体どういう意味になるのか、などということは一切規定されていない。
reinterpret_castには、できないこと
reinterpret_castが行えるキャストは、上記にすべて列挙した。それ以外の変換は、reinterpret_castでは行うことができない。これは、そもそもreinterpret_castの目的が、危険で実装依存なキャストのみを分離するという目的にあるので、それ以外の変換は、あえて行えないようになっている。
int main()
{
short value = 0 ;
// OK、標準型変換による暗黙の型変換
int a = value ;
// OK、static_castによる明示的な型変換
int b = static_cast<int>(value) ;
// エラー
// reinterpret_castでは、この型変換をサポートしていない
int c = reinterpret_cast<int>(value) ;
}
const_cast < 型名 > ( 式 )
const_castは、constとvolatileが異なる型の間の型変換を行う。constやvolatileを取り除くことや、付け加えることができる。
int main()
{
int const x = 0 ;
// エラー、constを取り除くことはできない。
int * error1 = &x ;
int * error2 = static_cast<int *>(&x) ;
// OK、ポインターの例
int * ptr = const_cast<int *>(&x) ;
// OK、リファレンスの例
int & ref = const_cast<int &>(x) ;
// constを付け加えることもできる。
int y = 0 ;
int const * cptr = const_cast<int const *>(&y) ;
}
ポインターへのポインターであっても、それぞれのconstを取り除くことができる。
int main()
{
int const * const * const * const c = nullptr ;
int *** ptr = const_cast<int ***>(c) ;
}
const_castは、constやvolatileのみを取り除く、または付け加えるキャストのみを行える。それ以外の型変換を行うことはできない。
int main()
{
int const x = 0 ;
// エラー、const以外の型変換を行っている。
short * ptr = const_cast<short *>(&x) ;
}
では、constを取り除くと同時に、他の型変換も行ないたい場合はどうするかというと、static_castや、reinterpret_castを併用する。
int main()
{
int const x = 0 ;
short * ptr1 =
static_cast<short *>(
static_cast<void *>(
const_cast<int *>(&x)
)
) ;
short * ptr2 = reinterpret_cast<short *>(const_cast<int *>(&x)) ;
}
const_castは、基本的に、ほとんどのconstをキャストすることができるが、キャストできないconstも存在する。たとえば、関数ポインターやメンバー関数ポインターに関するconstを取り除くことはできない。関数へのリファレンスも同様である。
void f( int ) {}
int main()
{
using type = void (*)(int) ;
type const ptr = nullptr ;
// エラー、関数ポインターはキャストできない
type p = const_cast<type>(ptr) ;
}
もちろん、関数の仮引数に対するconstをキャストすることや、constなメンバー関数を非constなメンバー関数にキャストすることなどもできない。
単項式は、オペランドをひとつしか取らないことより、そう呼ばれている。単項式の評価順序はすべて、「右から左」である。
単項演算子というカテゴリーには、六つの異なる演算子がまとめられている。*、&、+、-、!、~である。
* 単項演算子は、参照(indirection)である。オペランドは、オブジェクトへのポインターでなければならない。オペランドの型が、「Tへのポインター」であるとすると、式の結果は、lvalueのTである。
& 演算子は、オペランドのポインターを得る。オペランドの型がTであるとすると、結果は、prvalueのTへのポインターである。& 演算子は、オブジェクトだけではなく、関数にも適用できる。
int main()
{
int x = 0 ;
// & 演算子
// 変数xのオブジェクトへのポインターを得る。
int * ptr = &x ;
// * 演算子
// ポインターを参照する
*ptr ;
}
単項演算子の+と-は、オペランドの符号を指定する演算子である。
+ 単項演算子は、オペランドの値を、そのまま返す。オペランドの型には、数値型、非scoped enum型、ポインター型が使える。結果はprvalueである。
int main()
{
int x = +0 ; // xは0
+x ; // 結果は0
int * ptr = nullptr ;
+ptr ; // 結果はptrの値
}
ただし、オペランドには、整数のプロモーションが適用されるので、オペランドの型がcharやshort等の場合、int型になる。
short x = 0 ;
+x ; // int型の0
- 単項演算子は、オペランドの値を、負数にして返す。オペランドの型には、数値型と非scoped enum型が使える。+ 単項演算子と同じく、オペランドには整数のプロモーションが適用される。
-0 ; // 0
-1 ; // -1
- -1 ; // +1
- 単項演算子が、unsignedな整数型に使われた場合の挙動は、明確に定義されている。オペランドのunsignedな整数型のビット数をnとする。式の結果は、2nから、オペランドの値を引いた結果の値になる。
具体的な例を挙げるために、今、unsigned int型を16ビットだと仮定する。
// unsigned int型は16bitであるとする。
unsigned int x = 1 ;
// result = 216 - 1 = 65536 - 1 = 65535
unsigned int result = -x ;
x = 100 ;
// result = 216 - 100 = 65536 - 100 = 65436
result = -x ;
! 演算子は、オペランドをboolに変換し、その否定を返す。つまり、オペランドがtrueの場合はfalseに、falseの場合はtrueになる。
!true ; // false
!false ; // true
int x = 0 ;
!x ; // 0はfalseに変換される。その否定なので、結果はtrue
~ 演算子は、ビット反転とも呼ばれている。オペランドには、整数型と非scoped enum型が使える。式の結果は、オペランドの1の補数となる。すなわち、オペランドの各ビットが反転された値となる。オペランドには整数のプロモーションが適用される。
int x = 0 ;
// ビット列の各ビットを反転する
~x ;
++ 式
-- 式
ここでは、前置式のインクリメントとデクリメントについて解説する。後置式のインクリメントとデクリメントも参照。
前置式の++ 演算子は、オペランドに1を加算して、その結果をそのまま返す。オペランドは数値型かポインター型で、変更可能なlvalueでなければならない。式の結果はlvalueになる。
前置式の-- 演算子は、オペランドから1を減算する。それ以外は、++演算子と同じように動く。
int x = 0 ;
int result = ++x ;
// ここで、result = 1, x = 1
sizeof ( 未評価式 )
sizeof ( 型名 )
sizeof ... ( 識別子 )
sizeofとは、オペランドを表現するオブジェクトのバイト数を返す演算子である。オペランドは、未評価式か型名になる。
オペランドに型名を指定した場合、sizeof演算子は、型のオブジェクトのバイト数を返す。sizeof(char)、sizeof(signed char)、sizeof(unsigned char)は、1を返す。それ以外のあらゆる型のサイズは、実装によって定義される。たとえば、sizeof(bool)やsizeof(char16_t)やsizeof(char32_t)のサイズも、規格では決められていない。
// 1
sizeof(char) ;
// int型のオブジェクトのサイズ
sizeof(int) ;
オペランドに式を指定した場合、その式の結果の型のオブジェクトのバイト数を返す。式は評価されない。lvalueからrvalueへの型変換、配列からポインターへの型変換、関数からポインターへの型変換は行われない。
int f() ;
// int型のオブジェクトのサイズ
sizeof( f() ) ;
// int型のオブジェクトのサイズ
sizeof( 1 + 1 ) ;
関数呼び出しの式の結果の型は、関数の戻り値の型になる。
オペランドには、関数と不完全型を使うことはできない。関数は、そもそもオブジェクトではないし、不完全型は、そのサイズを決定できないからだ。「関数」は使えないが、関数呼び出しは「関数」ではないので使える。また、関数ポインターにも使える。
int f() ;
struct Incomplete ;
// 関数呼び出しは「関数」ではない
// sizeof(int) と同じ
sizeof( f() ) ;
// 関数ポインターはオブジェクトであるので、使える
// sizeof ( int (*)(void) ) と同じ
sizeof( &f ) ;
// エラー、関数を使うことはできない
sizeof( f ) ;
// エラー、不完全型を使うことはできない
sizeof( Incomplete ) ;
オペランドがリファレンス型の場合、参照される型のオブジェクトのサイズになる。
void f( int & ref )
{
// sizeof(int)と同じ
sizeof(int &) ;
sizeof(int &&) ;
sizeof( ref ) ;
}
オペランドがクラス型の場合、クラスのオブジェクトのバイト数になる。これには、アライメントの調整や、配列の要素として使えるようにするための実装依存のパディングなども含まれる。クラス型のサイズは、必ず1以上になる。これは、サイズが0では、ポインターの演算などに差し支えるからである。
オペランドが配列型の場合、配列のバイト数になる。これはつまり、要素の型のサイズ × 要素数となる。
// sizeof(int) * 10 と同じ
sizeof( int [10] ) ;
char a[10] ;
// sizeof(char) * 10 = 1 * 10 = 10
sizeof( a ) ;
// この型は配列ではなく、char
// sizeof(char)と同じ
sizeof( a[0] ) ;
sizeof...は、オブジェクトのバイト数とは、何の関係もない。sizeof...のオペランドには、パラメーターパックの識別子を指定できる。sizeof...演算子は、オペランドのパラメーターパックの引数の数を返す。sizeof...演算子の結果は定数で、型はstd::size_tである。
#include <cstddef>
#include <iostream>
template < typename... Types >
void f( Types... args )
{
std::size_t const t = sizeof...(Types) ;
std::size_t const a = sizeof...(args) ;
std::cout << t << ", " << a << std::endl ;
}
int main()
{
f() ; // 0, 0
f(1,2,3) ; // 3, 3
f(1,2,3,4,5) ; // 5, 5
}
確保関数と解放関数の具体的な実装方法については、動的メモリー管理を参照。
::opt new 型名 new初期化子opt
::opt new ( 式リスト ) 型名 new初期化子opt
newに必要な宣言の一部は、<new>ヘッダーで定義されているので、使う際は、これをincludeしなければならない。
new式は、型名のオブジェクトを生成する。newされる型は、完全型でなければならない。ただし、抽象クラスはnewできない。リファレンスはオブジェクトではないため、newできない。new式の結果は、型が配列以外の場合は、生成されたオブジェクトへのポインターを返す。型が配列の場合は、配列の先頭要素へのポインターを返す。
class C {} ;
int main()
{
// int型のオブジェクトを生成する
int * i = new int ;
// C型のオブジェクトを生成する
C * c = new C ;
}
newが、オブジェクトのためのストレージの確保に失敗した場合、std::bad_alloc例外がthrowされる。
int main()
{
try
{
new int ;
}
catch ( std::bad_alloc )
{
// newが失敗した
}
}
詳細なエラーについては、後述する。
newによって生成されるオブジェクトは、動的ストレージの有効期間を持つ。つまり、newによって作られたオブジェクトを破棄するためには、明示的にdeleteを使わなければならない。
int main()
{
int * ptr = new int ; // 生成
delete ptr ; // 破棄
}
new式の評価
new式に、new初期化子が指定されている場合、その式を評価する。次に、確保関数(allocation function)を呼び出して、オブジェクトの生成に必要なストレージを確保する。初期化を行ない、確保したストレージ上に、オブジェクトを構築する。そして、オブジェクトへのポインターを返す。
配列の生成
newで配列を生成する場合、要素数は、定数でなくても構わない。
void f( int n )
{
// 5個のint型の配列を生成する
// 要素数は定数
new int[5] ;
// n個のint型の配列を生成する
// 要素数は定数ではない
new int[n] ;
}
配列の配列、つまり多次元配列を生成する場合、配列型の最初に指定する要素数は、定数でなくても構わない。残りの要素数は、すべて定数でなければならない。
void f( int n )
{
// 要素数はすべて定数
new int[5][5][5] ;
// OK
// 最初の要素数は定数ではない
// 残りはすべて定数
new int[n][5][5] ;
new int [n] // 最初の要素数は定数でなくてもよい
[5][5] ; // 残りの要素数は定数でなければならない
// エラー
// 最初以外の要素数が定数ではない
new int[n][n][n] ;
new int[5][n][n] ;
new int[5][n][5] ;
}
配列の要素数が0の場合、newは、0個の配列を生成する。配列の要素数が負数であった場合の挙動は未定義である。
int main()
{
// OK
int * ptr = new int[0] ;
// もちろんdeleteしなければならない
delete [] ptr ;
// エラー
new int[-1] ;
}
もし、配列型の定数ではない要素数が、実装の制限以上の大きさである場合、ストレージの確保は失敗する。その場合、std::bad_array_new_length例外がthrowされる。要素数が定数であった場合は、通常通り、std::bad_alloc例外がthrowされる。
// int[n]のストレージを確保できないとする。
int main()
{
try
{
std::size_t n = std::numeric_limits<std::size_t>::max() ;
new int[n] ; // 要素数は定数ではない
}
catch ( std::bad_array_new_length )
{
// ストレージを確保できなかった場合
}
try
{
// numeric_limitsのメンバー関数maxはconstexpr関数なので、定数になる。
std::size_t const n = std::numeric_limits<std::size_t>::max() ;
new int[n] ; // 要素数は定数
}
catch ( std::bad_alloc )
{
// ストレージを確保できなかった場合
}
}
要素数が定数でない場合で、ストレージが確保できない場合のみ、std::bad_array_new_lengthがthrowされる。要素数が定数の場合は、通常通り、std::bad_allocがthrowされる。
オブジェクトの初期化
生成するオブジェクトの初期化は、new初期化子によって指定される。new初期化子とは、( 式リスト )か、初期化リストのいずれかである。new初期化子が指定された場合、オブジェクトは、直接初期化される。new初期化子が省略された場合、デフォルト初期化される。
struct C
{
C() {}
C(int) {}
C(int,int) {}
} ;
int main()
{
// new初期化子が省略されている
// デフォルト初期化
new C ;
// 直接初期化
new C(0) ;
new C(0, 0) ;
// 初期化リスト
new C{0} ;
new C{0,0} ;
}
組み込み型に対するデフォルト初期化は、「初期化しない」という挙動なので、注意を要する。初期化についての詳しい説明は、初期化子を参照。
型名としてのauto
newの型名がautoの場合、new初期化子は、( 代入式 )の形を取らなければならない。オブジェクトの型は、代入式の結果の型となる。オブジェクトは代入式の結果の値で初期化される。
int f() { return 0 ; }
int main()
{
// int型、値は0
new auto( 0 ) ;
// double型、値は0.0
new auto( 0.0 ) ;
// float型、値は0.0f
new auto( 0.0f ) ;
// int型、値は関数fの戻り値
new auto( f() ) ;
}
これは、auto指定子とよく似ている。
placement new
placement newとは、確保関数に追加の引数を渡すことができるnew式の文法である。これは、対応するnew演算子のオーバーロード関数を呼び出す。
void * operator new( std::size_t size, int ) throw(std::bad_alloc)
{ return operator new(size) ; }
void * operator new( std::size_t size, int, int ) throw(std::bad_alloc)
{ return operator new(size) ; }
void * operator new( std::size_t size, int, int, int ) throw(std::bad_alloc)
{ return operator new(size) ; }
int main()
{
new(1) int ; // operator new( sizeof(int), 1 )
new(1,2) int ; // operator new( sizeof(int), 1, 2 )
new(1,2,3) int ; // operator new( sizeof(int), 1, 2, 3 )
}
このように、newと型名の間に、通常の関数の実引数のリストのように、追加の引数を指定することができる。追加の引数は、operator newの二番目以降の引数に渡される。placement newの追加の引数は、ストレージを確保する方法を確保関数に指定するなどの用途に使える。
特殊なplacement new
C++には、あらかじめplacement newが二つ定義されている。operator new(std::size_t, const std::nothrow_t &) throw()と、operator new(std::size_t, void *) throw()である。
operator new(std::size_t, const std::nothrow_t &) throw()は、ストレージの確保に失敗しても例外を投げない特別な確保関数である。これには通常、std::nothrowが渡される。
// デフォルトで実装により定義される確保関数
// void * operator new(std::size_t, const std::nothrow_t &) throw() ;
int main()
{
// 失敗しても例外を投げない
int * ptr = new(std::nothrow) int ;
if ( ptr != nullptr )
{
// オブジェクトの生成に成功
// 参照できる
*ptr = 0 ;
}
delete ptr ;
}
nothrow版のnew演算子のオーバーロードは、ストレージの確保に失敗しても、例外を投げない。かわりに、nullポインターを返す。これは、newは使いたいが、どうしても例外を使いたくない状況で使うことができる。nothrow版のnewを呼び出した場合は、戻り値がnullポインターであるかどうかを確認しなければならない。
std::nothrow_tは、単にオーバーロード解決のためのタグに過ぎない。また、引数として渡しているstd::nothrowは、単に便利な変数である。
// 実装例
namespace std {
struct nothrow_t {} ;
extern const nothrow_t nothrow ;
}
operator new(std::size_t, void *) throw()は、非常に特別な確保関数である。この形のnew演算子はオーバーロードできない。このnew演算子は、ストレージを確保する代わりに、第二引数に指定されたポインターの指すストレージ上に、オブジェクトを構築する。第二引数のポインターは、オブジェクトの構築に必要なサイズやアライメント要求などの条件を満たしていなければならない。
一般に、placement newといえば、この特別なnew演算子の呼び出しを意味する。ただし、正式なplacement newという用語の意味は、追加の実引数を指定するnew式の文法である。
struct C
{
C(){ std::cout << "constructed." << std::endl ; }
~C(){ std::cout << "destructed." << std::endl ; }
} ;
int main()
{
// ストレージを自前で確保する
// operator newの返すストレージは、あらゆるアライメント要求を満たす
void * storage = operator new( sizeof(C) ) ;
// placement newによって、ストレージ上にオブジェクトを構築
C * ptr = new( storage ) C ;
// ストレージの解放の前に、デストラクターを呼び出す
ptr->~C() ;
// ストレージを自前で解放する
operator delete( storage ) ;
}
ストレージは自前で確保しなければならないので、通常通りdelete式を使うことはできない。デストラクターを自前で呼び出し、その後に、ストレージを自前で解放しなければならない。
ストレージは、動的ストレージでなくても構わない。ただし、アライメント要求には注意しなければならない。
struct C
{
int x ;
double y ;
} ;
int main()
{
// ストレージは自動変数
char storage [[align(C)]] [sizeof(C)] ;
// placement newによって、ストレージ上にオブジェクトを構築
C * ptr = new( storage ) C ;
// デストラクターはtrivialなので呼ぶ必要はない。
// ストレージは自動変数なので、解放する必要はない
}
この例では、sizeof(C)の大きさのchar配列の上にオブジェクトを構築している。アトリビュートを使い、アライメントを指定していることに注意。
このplacement newは、STLのアロケーターを実装するのにも使われている。
ストレージの確保に失敗した場合のエラー処理
確保関数がストレージの確保に失敗した場合、std::bad_alloc例外がthrowされる。placement newのstd::nothrow_tを引数に取る確保関数の場合は、戻り値のポインターが、nullポインターとなる。
int main()
{
try
{
new int ;
}
catch ( std::bad_alloc )
{
// エラー処理
}
int * ptr = new(std::nothrow) int ;
if ( ptr == nullptr )
{
// エラー処理
}
}
初期化に失敗した場合のエラー処理
newが失敗する場合は、二つある。ストレージが確保に失敗した場合と、オブジェクトの初期化に失敗した場合である。
たとえストレージが確保できたとしても、オブジェクトの初期化は、失敗する可能性がある。なぜならば、初期化の際に、コンストラクターが例外を投げるかもしれないからだ。
// 例外を投げるコンストラクターを持つクラス
struct Fail
{
Fail() { throw 0 ; }
} ;
int main()
{
try
{
new Fail ; // 必ず初期化に失敗する
}
catch ( int ) { }
}
コンストラクターが例外を投げた場合、newは、確保したストレージを、対応する解放関数(deallocation function)を呼び出して解放する。そして、コンストラクターの投げた例外を、そのまま外に伝える。
対応する解放関数とは何か。通常は、operator delete(void *)である。しかし、placement newを使っている場合は、最初の引数を除く、残りの引数の数と型が一致するoperator deleteになる。
// placement new
void * operator new( std::size_t size, int, int, int ) throw(std::bad_alloc)
{ return operator new(size) ; }
// placement delete
void operator delete( void * ptr, int, int, int ) throw()
{
std::cout << "placement delete" << std::endl ;
operator delete(ptr) ;
}
// 例外を投げるかもしれないクラス
struct Fail
{
Fail() noexcept(false) ; // 例外を投げる可能性がある
} ;
int main()
{
// コンストラクターが例外を投げた場合、
// operator delete( /*ストレージへのポインター*/, 1, 2, 3 )が呼ばれる
Fail * ptr = new(1, 2, 3) Fail ;
// operator delete(void *)が呼ばれる
delete ptr ;
}
初期化が失敗した場合のplacement deleteの呼び出しには、placement newに渡された追加の引数と、全く同じ値が渡される。
なお、delete式は通常通り、operator delete(void *)を呼び出す。たとえplacement newで確保したオブジェクトであっても、delete式では対応する解放関数は呼ばれない。あくまで、初期化の際に呼ばれるだけである。また、delete式から、placement deleteを呼び出す文法も存在しない。これは、「newの際に指定した情報を、deleteの際にまで保持しておくのは、ユーザー側にとっても実装側にとっても困難である」という思想に基づく。
確保関数の選択
new式が呼び出す確保関数は、以下の方法で選択される。
生成するクラスのメンバー関数に、operator newのオーバーロードがある場合、メンバー関数が選ばれる。メンバー関数によってオーバーロードされていない場合、グローバルスコープのoperator newが選ばれる。new式が、「::new」で始まる場合、たとえメンバー関数によるオーバーロードがあっても、グローバルスコープのoperator newが選ばれる。
// オーバーロードあり
struct A
{
void * operator new( std::size_t size ) throw(std::bad_alloc) ;
} ;
// オーバーロードなし
struct B { } ;
int main()
{
// A::operator newが選ばれる
new A ;
// ::operator newが選ばれる
new B ;
// ::operator newが選ばれる
::new A ;
}
配列の場合も同様である。配列の場合メンバー関数は、配列の要素のクラス型のメンバーから探される。
// オーバーロードあり
struct A
{
void * operator new[]( std::size_t size ) throw(std::bad_alloc) ;
} ;
// オーバーロードなし
struct B { } ;
int main()
{
// A::operator new[]が選ばれる
new A[1] ;
// ::operator new[]が選ばれる
new B1[1] ;
// ::operator new[]が選ばれる
::new A[1] ;
}
placement newの場合、追加の引数が、オーバーロード解決によって考慮され、最も最適なオーバーロード関数が選ばれる。
void * operator new( std::size_t size, int ) throw( std::bad_alloc ) ;
void * operator new( std::size_t size, double ) throw( std::bad_alloc ) ;
void * operator new( std::size_t size, int, int ) throw( std::bad_alloc ) ;
int main()
{
// operator new( std::size_t size, int )
new(0) int ;
// operator new( std::size_t size, double )
new(0.0) int ;
// operator new( std::size_t size, int, int )
new(1, 2) int ;
}
CV修飾されている型のnew
CV修飾子のある型もnewできる。特に変わることはない。
int main()
{
int const * ptr = new int const(0) ;
delete ptr ;
}
確保関数と解放関数の具体的な実装方法については、動的メモリー管理を参照。
::opt delete 式
::opt delete [ ] 式
new式によって確保したオブジェクトの寿命は、スコープにはとらわれない。オブジェクトを破棄したければ、delete式で解放しなければならない。
deleteのオペランドの値は、new式によって返されたポインターでなければならない。オブジェクトが配列ではない場合は、deleteを、配列の場合は、delete [ ]を使う。delete式の結果の型は、voidである。
int main()
{
int * ptr = new int ;
int * array_ptr = new int[1] ;
delete ptr ;
delete[] array_ptr ;
}
配列であるかどうかで、deleteとdelete[]を使い分けなければならない。これは間違えやすいので注意すること。
deleteのオペランドがクラスのオブジェクトであった場合、非explicitなユーザー定義の変換が定義されている場合、オブジェクトへのポインターに変換される。
struct C
{
operator int *() { return new int ; }
} ;
int main()
{
C c ;
// C::operator int *()を呼び出し、
// 戻り値を解放する。
delete c ;
}
delete式は、まず、ポインターの指し示すオブジェクトのデストラクターを呼び出す。次に、解放関数を呼び出して、ストレージを解放する。オブジェクトの指す型が、メンバー関数としてoperator deleteのオーバーロードを持つ場合、メンバー関数が呼ばれる。オーバーロードされたメンバー関数が存在しない場合、グローバルスコープのoperator deleteを呼び出す。delete式が、「::delete」で始まる場合、メンバー関数のオーバーロードの有無にかかわらず、グローバルスコープのoperator deleteを呼び出す。
// オーバーロードあり
struct A
{
void operator delete( void * ) throw() ;
} ;
// オーバーロードなし
struct B { } ;
int main()
{
A * a = new A ;
// A::operator delete(void*)を呼び出す
delete a ;
B * b = new B ;
// ::operator delete(void*)を呼び出す
delete b ;
a = new A ;
// ::operator delete(void*)を呼び出す
::delete a ;
}
オブジェクトが、placement newで確保されたとしても、呼び出す解放関数は、必ずoperator delete(void *)、もしくはoperator delete[](void *)となる。delete式では、placement deleteは呼び出されない。また、delete式には、placement deleteを呼び出すための文法も存在しない。どうしてもplacement deleteを呼び出したい場合は、手動でデストラクターを呼び出し、さらに手動でplacement deleteを呼び出すしかない。
// placement delete
void operator delete( void *, int ) throw() ;
struct C
{
C() {}
~C(){}
} ;
void f()
{
C * ptr = new C ;
// これでは、operator delete( void * )が呼び出される
delete ptr ;
// 疑似デストラクター呼び出し
ptr->~C() ;
// operator deleteの明示的な呼び出し
operator delete( ptr, 0 ) ;
}
alignof ( 型名 )
alignof式は、オペランドの型のアライメント要求を返す。オペランドの型は、完全なオブジェクト型か、その配列もしくはリファレンスでなければならない。式の結果は、std::size_t型の定数になる。
オペランドが、リファレンス型の場合、結果は参照される型のアライメント要求になる。配列の場合、結果は配列の要素の型のアライメント要求になる。
struct C
{
char c ; int i ; double d ;
} ;
void f()
{
// char型のアライメント要求を返す
alignof( char ) ;
// int型のアライメント要求を返す
alignof( int ) ;
// double型のアライメント要求を返す
alignof( double ) ;
// C型のアライメント要求を返す
alignof( C ) ;
}
noexcept ( 未評価式 )
noexcept演算子は、オペランドの式が、例外を投げる可能性のある式を含むかどうかを返す。noexcept演算子の結果の型はboolの定数で、例外を投げる可能性のある式を含まない場合trueを、含む場合falseを返す。オペランドの式は、評価されない。
結果がfalseとなる場合、すなわち、例外を投げる可能性のある式とは、以下の通りである。
throw式。
// false
noexcept( throw 0 ) ;
dynamic_cast式、dynamic_cast<T>(v)において、Tがリファレンス型で、実行時チェックが必要な場合。
struct Base { virtual void f() {} } ;
struct Derived : Base { } ;
void f( Base & ref )
{
// false
noexcept( dynamic_cast<Derived & >( ref ) ) ;
}
typeid式において、オペランドがglvalueで、実行時チェックが必要な場合。
struct Base { virtual void f() {} } ;
struct Derived : Base { } ;
void f( Base * ptr )
{
// false
noexcept( typeid( *ptr ) ) ;
}
関数、メンバー関数、関数ポインター、メンバー関数ポインターを呼び出す式において、呼び出す関数の例外指定が、無例外(non-throwing)でないもの。
void a() ;
void b() noexcept ; // non-throwing
void c() noexcept(true) ; // non-throwing
void d() noexcept(false) ;
void e() throw() ; // non-throwing
void f() throw(int) ;
int main()
{
noexcept( a() ) ; // false
noexcept( b() ) ; // true
noexcept( c() ) ; // true
noexcept( d() ) ; // false
noexcept( e() ) ; // true
noexcept( f() ) ; // false
}
関数を、「呼び出す式」というのは、関数を間接的に呼び出す場合も該当する。たとえば、new式は確保関数を呼び出すので、関数を呼び出す式である。その場合の結果は、呼び出される確保関数の例外指定に依存する。
int main()
{
// ::operator new( std::size_t ) throw( std::bad_alloc) を呼び出す
std::cout << noexcept( new int ) ; // false
// ::operator new( std::size_t, std::nothrow_t ) throw() を呼び出す
std::cout << noexcept( new(std::nothrow) int ) ; // true
}
もちろん、演算子のオーバーロード関数も、「関数」である。従って、演算子のオーバーロード関数を呼び出す式は、関数を呼び出す式である。
struct C
{
C operator +( C ) ;
C operator -( C ) noexcept ;
} ;
int main()
{
int i = 0 ;
noexcept( i + i ) ; // true
C c ;
noexcept( c + c ) ; // false
noexcept( c - c ) ; // true
}
その他にも、関数を間接的に呼び出す可能性のある式というのは、非常に多いので、注意しなければならない。
関数のオーバーロード解決は静的に行われるので、当然、呼び出される関数に応じて結果も変わる。
void f(int) noexcept ;
void f(double) ;
int main()
{
noexcept( f(0) ) ; // true
noexcept( f(0.0) ) ; // false
}
例外を投げる可能性のある式を「含む」というのは、たとえその式が絶対に評価されないでも、例外を投げる可能性があるとみなされる。例えば、
noexcept( true ? 0 : throw 0 ) ; // false
このnoexceptのオペランドの式は、もし評価された場合、決して例外を投げることがない。しかし、例外を投げる可能性のある式を含んでいるので、noexceptの結果はfalseとなる。
上記以外の場合、noexceptの結果はtrueとなる。
struct Base { } ;
struct Derived : Base { } ;
int main()
{
noexcept( 0 ) ; // true
Derived d ;
noexcept( d ) ; // true
noexcept( dynamic_cast<Base &>( d ) ) ; // true
noexcept( typeid( d ) ) ; // true
}
注意:C形式のキャストには様々な問題があるので、使ってはならない。
( 型名 ) 式
これは、悪名高いC形式のキャストである。
int main()
{
int i = 0 ;
double * ptr = (double *) &i ;
}
C形式のキャストは、static_castとreinterpret_castとconst_castを組み合わせた働きをする。組み合わせは、以下の順序で決定される。
- const_cast
- static_cast
- static_castとconst_cast
- reinterpret_cast
- reinterpret_castとconst_cast
上から下に評価していき、変換できる組み合わせが見つかったところで、そのキャストを使って変換する。
ただし、C形式のキャストでは、static_castに特別な変更を三つ加える。クラスのアクセス指定を無視できる機能である。
派生クラスへのポインターやリファレンスから、基本クラスへのポインターやリファレンスに変換できる。文字通り変換できる。アクセス指定などは考慮されない。
struct Base { } ;
struct Derived : private Base { } ;
int main()
{
Derived d ;
Base & ref1 = (Base &) d ; // OK
Base & ref2 = static_cast<Base &>(d) ; // ill-formed
}
このため、publicではない基本クラスにアクセスできてしまう。
派生クラスのメンバーへのポインターから、曖昧ではない非virtualな基本クラスのメンバーへのポインターに変換できる。文字通り変換できる。アクセス指定などは考慮されない。
struct Base { } ;
struct Derived : private Base { int x ; } ;
int main()
{
int Base::* ptr1 = (int Base::*) &Derived::x ; // OK
int Base::* ptr2 = static_cast<int Base::*>(&Derived::x) ; // ill-formed
}
これも、アクセス指定を無視できてしまう。
曖昧ではなく非virtualな基本クラスのポインターやリファレンスあるいはメンバーへのポインターは、派生クラスのポインターやリファレンスあるいはメンバーへのポインターに変換できる。文字通り変換できる。アクセス指定などは考慮されない。
struct Base { int x ; } ;
struct Derived : private Base { } ;
int main()
{
Derived d ;
d.x = 0 ; // ill-formed. アクセス指定のため
int Derived::* ptr = (int Derived::*) &Base::x ; // well-formed.
d.*ptr = 0 ; // well-formed. C形式のキャストを使ったため、アクセス指定を無視できている
}
C形式のキャストでしかできないキャストとは、クラスのアクセス指定を無視し、しかもクラス階層のナビゲーションを行うキャストのことである。
これらのキャストは、reinterpret_castでもできる。ただし、reinterpret_castは、クラス階層のナビゲーションを行わないので、正しく動かない。static_castは、クラス階層のナビゲーションを行うので、正しく動く。
アクセス指定を無視できるキャストをしなければならない場合というのは、現実には存在しないはずである。アクセス指定を無視するぐらいならば、最初からpublicにしておけばいい。
reinterpret_castは必要である。C++が必要とされる環境では、ポインターの内部的な値を、そのまま別の型のポインターとして使わなければならない場合も存在する。また、既存のCのコードとの互換性のため、const_castも残念ながら必要である。しかし、アクセス指定は、C++に新しく追加された概念であるので、互換性の問題も存在しないし、また、アクセス指定を無視しなければならない場合というのも、全く考えられない。従って、アクセス指定を無視できるという理由で、C形式のキャストを使ってはならない。
そもそも、C形式のキャストは根本的に邪悪であるので、使ってはならない。C形式のキャストの問題点は、できることが多すぎるということだ。安全なキャストも、危険なキャストも、全く同じ文法で行うことができる。C++では、この問題を解決するために、キャストを三つに分けた。static_cast、reinterpret_cast、const_castである。C++では、この新しい形式のキャストを使うべきである。以下にその概要と簡単な使い分けをまとめる。
static_castは、ほとんどが安全なキャストである。static_castは、型変換を安全にするため、値を変えることもある。値を変更するので、static_castは、クラス階層のナビゲーションを行うことができる。派生クラスと基本クラスとの間のポインターの型変換は、ポインターの内部的な値が変わる可能性があるからだ。ポインターの値は、もとより実装依存であるが、最も多くの環境で再現できるコードは、複数の基本クラスを使うものだ。
struct Base1 { int x ; } ;
struct Base2 { int x ; } ;
struct Derived : Base1, Base2 { } ;
int main()
{
Derived d ;
Derived * ptr = &d ;
// 基本クラスへのキャスト
Base1 * base1 = static_cast<Base1 *>( ptr ) ;
Base2 * base2 = static_cast<Base2 *>( ptr ) ;
// 派生クラスへのキャスト
Derived * d1 = static_cast<Derived *>( base1 ) ;
Derived * d2 = static_cast<Derived *>( base2 ) ;
// 派生クラスのポインターの値
std::printf( "Derived *: %p\n", ptr ) ;
// 基本クラスのポインターの値は同じか?
std::printf( "Base1 *: %p\n", base1 ) ;
std::printf( "Base2 *: %p\n", base2 ) ;
// 派生クラスに戻した場合はどうか?
std::printf( "from Base1 * to Derived *: %p\n", d1 ) ;
std::printf( "from Base1 * to Derived *: %p\n", d2 ) ;
}
複数の基本クラスの場合、基本クラスのサブオブジェクトが複数あるので、派生クラスと基本クラスのポインターの間で、同じ値を使うことができない。従って、基本クラスへのポインターにキャストするには、ストレージ上の、その基本クラスのサブオブジェクトを指すポインターを返さなければならない。また、派生クラスへのポインターにキャストするには、値を戻さなければならない。
このため、クラス階層のナビゲーションには、static_castかdynamic_castを用いなければならない。
reinterpret_castは、危険で愚直なキャストである。reinterpret_castは、値を変えない。ただ、その値の型だけを変更する。reinterpret_castは、クラス階層のナビゲーションができない。
const_castは、CV修飾子を外すキャストである。
もし、どのキャストを使うべきなのか判断できない場合は、まずstatic_castを使っておけば問題はない。もし、static_castが失敗した場合、本当にそのキャストは安全なのかということを確かめてから、reinterpret_castを使うべきである。const_castは、既存のCのコードの利用以外に使ってはならない。
式 .* 式
式 ->* 式
メンバーへのポインター演算子は、「左から右」に評価される。
メンバーへのポインター演算子は、クラスのメンバーへのポインターを使って、クラスのオブジェクトのメンバーにアクセスするための演算子である。クラスのメンバーへのポインターを参照するためには、参照するクラスのオブジェクトが必要である。
.*演算子の第一オペランドには、クラスのオブジェクトを指定する。->*演算子の第一オペランドには、クラスへのポインターを指定する。第二オペランドには、クラスのメンバーへのポインターを指定する。
struct C
{
int member ;
} ;
int main()
{
int C::* mem_ptr = &C::member ;
C c ;
c.*mem_ptr = 0 ;
C * ptr = &c ;
ptr->*mem_ptr = 0 ;
}
メンバー関数の呼び出しの際は、演算子の優先順位に気をつけなければならない。
struct C
{
void member() {}
} ;
int main()
{
void (C::* mem_ptr)() = &C::member ;
C c ;
(c.*mem_ptr)() ;
C * ptr = &c ;
(ptr->*mem_ptr)() ;
}
なぜならば、メンバーへのポインター演算子の式より、関数呼び出し式の優先順位の方が高いので、c.*mem_ptr()という式は、c.*( mem_ptr() )という式に解釈されてしまう。これは、mem_ptrという名前に対して、関数呼び出し式を適用した後、その結果を、クラスのメンバーへのポインターとして使う式である。このように解釈されることを避けるために、括弧式を使わなければならない。
その他の細かいルールについては、クラスメンバーアクセスと同じである。
式 * 式
式 / 式
式 % 式
乗除算の演算子は、「左から右」に評価される。
*演算子と/演算子のオペランドは、数値型かunscoped enum型でなければならない。%演算子のオペランドは、整数型かunscoped enum型でなければならない。オペランドには、通常通り数値に関する標準型変換が適用される。式を参照。
*演算子は、乗算を意味する。
/演算子は、除算を意味する。%演算子は、第一オペランドを第二オペランドで割った余りを意味する。第二オペランドの値が0の場合の挙動は未定義である。/演算子の結果の型が整数の場合、小数部分は切り捨てられる。
int main()
{
2 * 3 ; // 6
10 / 5 ; // 2
3 % 2 ; // 1
3 / 2 ; // 結果は整数型、小数部分が切り捨てられるので、結果は1
3.0 / 2.0 ; // 結果は浮動小数点数型の1.5
}
以下は間違っている例である。
// このコードは間違っている例
int main()
{
// ゼロ除算
1 / 0 ;
// %演算子のオペランドに浮動小数点数型は使えない
3.0 % 2.0 ;
}
式 + 式
式 - 式
加減算の演算子は、「左から右」に評価される。
両方のオペランドが数値型の場合
+演算子は、加算を意味する。-演算子は、減算を意味する。-演算子の減算とは、第二オペランドの値を第一オペランドから引くことである。結果の型には、通常通り数値型に関する標準型変換が行われる。
int main()
{
1 + 1 ; // 2
1 - 1 ; // 0
}
オペランドがポインター型の場合
まず、ポインターの型は、完全に定義されたオブジェクトでなければならない。ポインターは、配列の要素を指し示しているものとみなされる。たとえ実際には配列の要素を指していないとしても、配列の要素を指しているものとみなされる。
+演算子の片方のオペランドがポインター型の場合、もう片方は、整数型でなければならない。-演算子は、両方のオペランドが同じポインター型か、左オペランドがポインター型で右オペランドが整数型でなければならない。
int main()
{
int array[3] ;
int * ptr = &array[1] ;
// OK
ptr + 1 ;
1 + ptr ;
ptr + (-1) ;
(-1) + ptr ;
ptr - ptr ;
ptr - 1 ;
ptr - (-1) ;
// エラー
ptr + ptr ; // +演算子の両オペランドがポインターとなっている
1 - ptr ; // -演算子の左オペランドが整数で右オペランドがポインターとなっている
}
ポインターと整数の加減算の結果の型は、ポインターの型である。結果の値は、ポインターが指す要素に対する配列中の添字に、整数を加減算した要素を指すものとなる。もし、ポインターが配列の添字でi番目の要素を指し示している場合、このポインターに整数nを加算することは、i + n番目の要素を指し示すことになる。同様にして、整数nを減算することは、i - n番目の要素を指し示すことになる。
int main()
{
int array[10] ;
int * ptr = &array[5] ;
ptr + 2 ; // &array[5 + 2]と同じ
ptr - 2 ; // &array[5 - 2]と同じ
}
もし、ポインターが、配列の最後の要素を指している場合、これに1を加えると、結果のポインターは配列の最後の要素のひとつ後ろを指すことになる。ポインターが配列の最後の要素のひとつ後ろを指している場合、これから1を引くと、結果のポインターは配列の最後の要素を指すことになる。
int main()
{
int array[10] ;
// 配列の最後の要素を指す
int * ptr = &array[9] ;
// 配列の最後の要素のひとつ後ろを指す
int * one_past_the_last = ptr + 1 ;
// 配列の最後の要素を指す
int * last = one_past_the_last - 1 ;
}
配列の最後の要素を指しているポインターに1を加算して、最後の要素の一つ後の要素を指すようにしても、規格上、ポインターの値のオーバーフローは起こらないと保証されている。2つ目以降の要素を指し示した場合、挙動は未定義である。
int main()
{
int a[1] ;
int * p1 = &a[0] ; // 最後の要素
int * p2 = p1 + 1 ; // OK、最後の一つ後の要素
int * p3 = p2 + 1 ; // 挙動は未定義
}
上の例で、もし、ポインターp2を参照した場合、挙動は未定義だが、p2自体は未定義ではない。p3は未定義である。
ポインター同士を減算した場合、結果は、ポインターの指す配列の添字の差になる。ポインターPが配列の添字でi番目の要素を差しており、ポインターQが配列の添字でj番目の要素を指している場合、P - Qは、i - jとなる。配列の添字は、0から始まることに注意。両方のポインターが同じ配列上の要素を差していない場合、挙動は未定義である。
int main()
{
int array[10] ;
int * P = &array[2] ;
int * Q = &array[7] ;
P - Q ; // 2 - 7 = -5
Q - P ; // 7 - 2 = 5
}
ポインター同士の減算の結果の型は、実装依存であるが、<cstddef>ヘッダーで定義されている、std::ptrdiff_tと同じ型になる。
0という値が、ポインターに足し引きされた場合、結果は、そのポインターの値になる。
void f( int * ptr )
{
ptr == ptr + 0 ; // true
ptr == ptr - 0 ; // true
}
式 << 式
式 >> 式
シフト演算子のオペランドは、整数型かunscoped enum型でなければならない。オペランドには、整数のプロモーションが行われる。結果の型は、整数のプロモーションが行われた後のオペランドの型になる。
左シフト、E1 << E2の結果は、E1をE2ビット、左にシフトしたものとなる。シフトされた後のビットは、0で埋められる。もし、E1の型がunsignedならば、結果の値は、E1 × 2E2を、E1の最大値+1で剰余したものとなる。
// コメント内の値は2進数である。
int main()
{
// 1101
unsigned int bits = 9 ;
bits << 1 ; // 11010
bits << 2 ; // 110100
}
E1の型がsignedの場合、E1が負数でなく、E1 × 2E2が表現可能であれば、その値になる。その他の場合は未定義である。これは、signedな整数型の内部表現が2の補数であるとは保証していないので、このようになっている。
// コメント内の値は2進数である
int main()
{
// 1101
int bits = 9 ;
bits << 1 ; // 11010
bits << 2 ; // 110100
-1 << 1 ; // 結果は未定義
}
右シフト、E1 >> E2の結果は、E1をE2ビット、右にシフトしたものとなる。もし、E1の型がunsignedか、signedで正の数ならば、結果の値は、E1 ÷ 2E2の整数部分になる。
// コメント内の値は2進数である
int main()
{
// 1101
unsigned int value = 9 ;
value >> 1 ; // 110
value >> 2 ; // 11
int signed_value = 9 ;
signed_value >> 1 ; // 110
signed_value >> 2 ; // 11
}
E1の型がsignedで、値が負数の場合、挙動は未定義である。
int main()
{
-1 >> 1 ; // 結果は未定義
}
右オペランドの値が負数であったり、整数のプローモーション後の左オペランドのビット数以上の場合の挙動は未定義である。
// この環境では、1バイトは8ビット
// sizeof(unsigned int)は2とする。
// すなわち、この環境では、unsigned intは16ビットとなる。
int main()
{
unsigned int value = 1 ;
value << -1 ; // 未定義
value >> -1 ; // 未定義
value << 16 ; // 未定義
value >> 16 ; // 未定義
value << 17 ; // 未定義
value >> 17 ; // 未定義
}
シフト演算には、未定義の部分が非常に多い。ただし、多くの現実の環境では、何らかの具体的な意味が定義されていて、時として、そのような未定義の挙動に依存したコードを書かなければならない場合がある。その場合、特定の環境に依存したコードだという正しい認識を持たなければならない。
式 < 式
式 > 式
式 <= 式
式 >= 式
関係演算子は「左から右」に評価される。
関係演算子のオペランドには、数値型、enum型、ポインター型を使うことができる。各演算子の意味は、以下のようになっている。
- A < B
- AはBより小さい
- A > B
- AはBより大きい
- A <= B
- AはBより小さいか、等しい
- A >= B
- AはBより大きいか、等しい
結果の型はboolとなる。両オペランドが数値型かenum型の場合、不等号の関係が正しければtrueを、そうでなければfalseを返す。
void f( int a, int b )
{
a < b ;
a > b ;
a <= b ;
a >= b ;
}
式の結果の型はboolである。
ポインター同士の比較に関しては、未規定な部分が多い。ここでは、規格により保証されていることだけを説明する。
同じ型の二つのポインター、pとqが、同じオブジェクトか関数を指している場合、もしくは、同じ配列の最後の要素のひとつ後の要素を指している場合、もしくは、両方ともnullの場合は、p<=qとp>=qはtrueとなり、p<qとp>qはfalseとなる。
int main()
{
int object = 0 ;
int * p = &object ;
int * q = &object ;
p <= q ; // true
p >= q ; // true
p < q ; // false
p > q ; // false
}
同じ型の二つのポインター、pとqが、異なるオブジェクトを差しており、そのオブジェクトは同じオブジェクトのメンバーではなく、また同じ配列内の要素ではなく、異なる関数でもなく、あるいは、どちらか片方の値のみがnullの場合、p<q, p>q, p<=q, p>=qの結果は、未規定である。
int main()
{
int object1 ;
int object2 ;
int * p = &object1 ;
int * q = &object2 ;
p <= q ; // 結果は未規定
p >= q ; // 結果は未規定
p < q ; // 結果は未規定
p > q ; // 結果は未規定
p < nullptr ; // 結果は未規定
}
同じ型の二つのポインター、pとqが、同じ配列の要素を指している場合、添字の大きい要素の方が、より大きいと評価される。
int main()
{
int array[2] ;
int * p = &array[0] ;
int * q = &array[1] ;
p < q ; // true
p > q ; // false
p <= q ; // true
p >= q ; // false
これと同様に、pとqが指しているものが、同じ型の同じクラスのオブジェクトのサブオブジェクトである場合は、同じアクセスコントロール下にある場合、後に宣言されたメンバーの方が、ポインター同士の比較演算では、大きいと評価される。
struct S
{
// 同じアクセスコントロール下
int a ;
int b ; // bが後に宣言されている
} ;
int main()
{
S object ;
// 同じオブジェクトのサブオブジェクト
int * p = &object.a ;
int * q = &object.b ;
p < q ; // true
p > q ; // false
} ;
これと似ているが、ただしクラスのメンバーのアクセスコントロールが異なる場合、結果は未規定である。
struct S
{
public :
int a ;
private :
int b ;
void f()
{
&a < &b ; // 結果は未規定
}
} ;
二つのポインター、pとqが、unionの同じオブジェクトの非staticなデータメンバーを指している場合、等しいと評価される。
union Object
{
int x ;
int y ;
} ;
int main()
{
Object object ;
int * p = &object.x ;
int * q = &object.y ;
p < q ; // false
p > q ; // false
p <= q ; // true
p >= q ; // true
p == q ; // true
}
二つのポインターが、同じ配列内の要素を指している場合、添字の大きい要素を指すポインターが、大きいと評価される。また、これはどちらか片方のポインターが、配列の範囲を超えていても、評価できる。
int main()
{
int a[2] ;
int * p1 = &a[0] ;
int * p2 = &a[1] ;
p1 < p2 ; // true
int * p3 = p2 + 1 ; // p3は配列の範囲外を指す
p1 < p3 ; // OK、結果はtrue
}
voidへのポインター型は、比較することができる。また、片方のオペランドがvoidへのポインター型で、もう片方が別のポインター型である場合、もう片方のオペランドが、標準型変換によってvoidへのポインター型に変換されるので、比較することができる。もし、両方のポインターが、同じアドレスであった場合かnullポインターの場合は、等しいと評価される。それ以外は、未規定である。
int main()
{
int object = 0 ;
int * ptr = &object ;
void * p = ptr ;
void * q = ptr ;
p < q ; // false
p > q ; // false
p <= q ; // true
p >= q ; // true
// 標準型変換によって、別のポインター型とも比較できる
p <= ptr ; // true
}
これ以外の比較の結果は、すべて未規定となっている。未定義ではなく、未規定なので、実装によっては、意味のある結果を返すこともある。しかし、実装に依存する挙動なので、移植性に欠ける。
式 == 式
式 != 式
==演算子(等しい)と、!=演算子(等しくない)は、関係演算子とオペランドや結果の型、評価の方法は同じである。ただし比較の意味は、「等しい」か、「等しくない」かである。
int main()
{
1 == 1 ; // true
1 != 1 ; // false
1 == 2 ; // false
1 != 2 ; // true
}
同じ型のポインターの場合、ともにアドレスが同じか、ともにnullポインターの場合、trueと評価される。
==演算子は、代入演算子である=演算子と間違えやすいので、注意しなければならない。
void f( int x )
{
if ( x = 1 ) // 間違い
{
// 処理
} else {
// 処理
}
}
この例では、if文の条件式の結果は、代入式の結果となってしまう。それは、1であるので、このif文は常にtrueであると評価されてしまう。
式 & 式
ビット列論理積演算子は、両オペランドの各ビットごとの論理積(AND)を返す。オペランドは整数型か、unscoped enum型でなければならない。
式 ^ 式
ビット列排他的論理和演算子は、両オペランドの各ビットごとの排他的論理和(exclusive OR)を返す。オペランドは整数型か、unscoped enum型でなければならない。
式 | 式
ビット列論理和演算子は、両オペランドの各ビットごとの論理和(inclusive OR)を返す。オペランドは整数型か、unscoped enum型でなければならない。
式 && 式
&&演算子は「左から右」に評価される。
論理積演算子は、オペランドの論理積を返す演算子である。両オペランドはboolに変換される。結果の型はboolである。両方のオペランドがtrueであれば、結果はtrue。それ以外はfalseとなる。
true && true ; // true
true && false ; // false
false && true ; // false
false && false ; // false
第一オペランドを評価した結果がfalseの場合、第二オペランドは評価されない。なぜならば、第一オペランドがfalseであれば、第二オペランドを評価するまでもなく、結果はfalseであると決定できるからである。
bool f() { return false ; }
bool g() { return true ; }
int main()
{
// g()は呼ばれない。結果はfalse
f() && g() ;
}
この例では、第一オペランドである関数fの呼び出しはfalseを返すので、第二オペランドの関数gの呼び出しが評価されることはない。つまり、関数gは呼ばれない。
第二オペランドが評価される時、第一オペランドの評価によって生じた値の計算や副作用は、すべて行われている。
int main()
{
int value = 0 ;
++value // 値は1になるので、trueと評価される
&&
value ; // 値はすでに1となっているので、trueと評価される
}
式 || 式
||演算子は、「左から右」に評価される。
論理和演算子は、オペランドの論理和を返す演算子である。両オペランドはboolに変換される。結果の型はboolである。オペランドが片方でもtrueと評価される場合、結果はtrueとなる。両オペランドがfalseの場合に、結果はfalseとなる。
int main()
{
true || true ; // true
true || false ; // true
false || true ; // true
false || false ; // false
}
第一オペランドを評価した結果がtrueの場合、第二オペランドは評価されない。なぜならば、第一オペランドがtrueであれば、第二オペランドを評価するまでもなく、結果はtrueとなるからである。
bool f() { return true ; }
bool g() { return false ; }
int main()
{
// g()は呼ばれない。結果はtrue
f() || g() ;
}
論理積と同じように、第二オペランドが評価される場合、第一オペランドの評価によって生じた値の計算や副作用は、すべて行われている。
式 ? 式 : 代入式
条件演算子は「左から右」に評価される。
条件演算子は、三つのオペランドを取る。C++には他に三つのオペランドを取る演算子がないことから、三項演算子といえば、条件演算子の代名詞のように使われている。しかし、正式名称は条件式であり、演算子の名称は条件演算子である。
条件演算子の第一オペランドはboolに変換される。値がtrueであれば、第二オペランドの式が評価され、その結果が返される。値がfalseであれば、第三オペランドの式が評価され、その結果が返される。第二オペランドと第三オペランドは、どちらか片方しか評価されない。
bool cond() ;
int e1() ;
int e2() ;
int main()
{
true ? 1 : 2 ; // 1
false ? 1 : 2 ; // 2
// 関数condの戻り値によって、関数e1、あるいはe2が呼ばれ、その戻り値が返される。
// e1とe2は、どちらか片方しか呼ばれない。
cond() ? e1() : e2() ;
}
実は、条件演算子は見た目ほど簡単ではない。特に、結果の型をどのようにして決定するかということが、非常に難しい。ここでは、結果の型を決定する完全な詳細は説明しないが、特に重要だと思われる事を取りあげる。
条件演算子の第二第三オペランドには、結果がvoid型となる式を使うことができる。
void f() {}
int main()
{
true ? f() : f() ;
int * ptr = new int ;
true ? delete ptr : delete ptr ;
true ? throw 0 : throw 0 ;
}
片方のオペランドがvoidで、もう片方がvoidではない場合、エラーである。
void f() {}
int main()
{
true ? 0 : f() ; // エラー
true ? f() : 0 ; // エラー
}
ただし、片方のオペランドがthrow式の場合に限り、もう片方のオペランドに、voidではない式でも使うことができる。結果はprvalueの値で、型はthrow式ではない方のオペランドの型になる。
void f() {}
int main()
{
// OK
// xに0を代入する
int x = true ? 0 : throw 0 ;
// エラー
// 戻り値に123を代入しようとしているが、prvalueには代入できない
(true ? x : throw 0) = 123 ;
true ? throw 0 : f() ; // OK
}
両オペランドが、ともに同じ値カテゴリーで、同じ型の場合は、条件演算子の結果は、その値カテゴリーと型になる。
int f() { return 0 ; }
int main()
{
int x = 0 ;
// 両オペランドとも、lvalueのint型
// 結果はlvalueのint
( true ? x : x ) = 0 ; // lvalueなので代入も可能
// 両オペランドとも、xvalueのint型
// 結果はxvalueのint
true ? std::move(x) : std::move(x) ;
// 両オペランドとも、prvalueのint型
// 結果はprvalueのint
true ? f() : f() ;
}
もし、オペランドの値カテゴリーや型が違う場合、暗黙の型変換によって、お互いの型と値カテゴリーを一致させようという試みがなされる。この変換の詳細は、非常に複雑で、通常は意識する必要はないため、本書では省略する。
式 代入演算子 式
代入演算子:以下のうちどれか
= *= /= %= += -= >>= <<= &= ^= |=
代入演算子(=)と、複合代入演算子は、「右から左」に評価される。
代入演算子は、左側のオペランドに、右側のオペランドの値を代入する。左側のオペランドは変更可能なlvalueでなければならない。結果として、左側のオペランドのlvalueを返す。
int main()
{
int x ;
x = 0 ;
}
初期化と混同しないように注意。
int main()
{
int x = 0 ; // これは初期化
x = 0 ; // これは代入
}
=を代入演算子といい、その他の演算子を、複合代入演算子という。
クラスの代入に関する詳細は、クラスオブジェクトのコピーとムーブや、オーバーロードの代入を参照。
複合代入演算子の式、E1 op = E2は、E1 = E1 op E2と同じである。ただし、E1という式は、一度しか評価されない。opには、任意の複合代入演算子の一部が入る。
int main()
{
int x = 0 ;
x += 1 ; // x = x + 1と同じ
x *= 2 ; // x = x * 2と同じ
}
右側のオペランドには、初期化リストを使うことができる。
左側のオペランドがスカラー型の場合、ある型Tの変数をxとすると、x = {v}という式は、x = T(v)という式と同じ意味になる。ただし、初期化リストなので、縮小変換は禁止されている。x = {}という式は、x = T()という式と同じ意味になる。
int main()
{
int x ;
x = {1} ; // x = int(1) と同じ
x = {} ; // x = int()と同じ
short s ;
s = {x} ; // エラー、縮小変換は禁止されている。
}
それ以外の場合は、初期化リストを実引数として、ユーザー定義の代入演算子が呼び出される。
struct C
{
C(){}
C( std::initializer_list<int> ) {}
} ;
int main()
{
C c ;
c = { 1, 2, 3 } ;
}
式 , 式
コンマ演算子は、「左から右」に評価される。
コンマ演算子は、まず左のオペランドの式が評価され、次に、右のオペランドの式が評価される。左のオペランドの式を評価した結果は破棄され、右のオペランドの結果が、コンマ演算子の結果として、そのまま返される。結果の型や値、値カテゴリーは、右のオペランドの式を評価した結果と全くおなじになる。
int main()
{
1, 2 ; // 2
1, 2, 3, 4, 5 ; // 5
}
右のオペランドの式が評価される前に、左のオペランドの式の値計算や副作用は、すでに行われている。
int f() ;
int g() ;
int main()
{
int i = 0 ;
// 左のオペランドのiは、すでにインクリメントされている。
++i, i ;
// 関数gが呼ばれる前に、関数fはすでに呼ばれ終わっている。
f() , g() ;
}
コンマが特別な意味を持つ場面では、コンマ演算子を使うには、明示的に括弧で囲まなければならない。コンマが特別な意味を持つ場面には、例えば、関数の実引数リストや、初期化リストなどがある。
void f(int, int, int) {}
int main()
{
int x ;
// 括弧が必要
f( 1, (x=0, x), 2) ;
}
この例では、関数fは三つの引数を取る。二つめの引数は、括弧式に囲まれたコンマ演算子の式である。これは変数xに0を代入した後、そのxを引数として渡している。
定数式(constant expression)とは、値がコンパイル時に決定できる式のことである。定数式かどうかということは、C++のいくつかの場面で、重要になってくる。例えば、配列を宣言する時、要素数は定数式でなければならない。
int main()
{
// 整数リテラルは定数式
int a[5] ;
// const修飾されていて、初期化式が定数式であるオブジェクトは定数式
int const n = 5 ;
int b[n] ; // OK
int m = 5 ; // これは定数式ではない
int c[m] ; // エラー
}
注意:この解説は、C++14のドラフトN3797を参考にしている。正式なC++14規格では変更される可能性がある。また、C++11ではない。
ある式eを評価した際に、以下に挙げる式を評価しない場合、式eはコア定数式(core constant expression)である。
-
this、ただし、eの一部として評価されるconstexpr関数かconstexprコンストラクター内を除く
-
リテラルクラスのconstexprコンストラクター、constexpr関数、トリビアルデストラクターの暗黙の呼び出しを除く、関数の呼び出し
-
未定義のconstexpr関数か未定義のconstexprコンストラクターの呼び出し
-
実装の制約を超える式
実装の制約とは、仮引数やテンプレート仮引数の数の最大値のような、コンピューターが有限であることに起因する様々な制約。
-
未定義の挙動を含む処理
例えば、符号付き整数のオーバーフローや、一部のポインター演算や、ゼロ除算や、一部のシフト演算など。
-
ラムダ式
-
lvalueからrvalueへの変換、ただし以下の場合を除く
-
lvalueからrvalueへの変換か、unionの使われていないメンバーかサブオブジェクトを参照するglvalueの変更
-
リファレンス型の変数かデータメンバーを参照する名前、ただし、リファレンスが以下のいずれかの方法で初期化された後の場合を除く。
-
定数式で初期化された場合
-
式eの評価とともに寿命が始まったオブジェクトの非staticデータメンバー
-
cv void *からオブジェクトへのポインター型への型変換
-
dynamic_cast
-
reinterpret_cast
-
疑似デストラクター呼び出し
-
オブジェクトの変更、ただし、式eの評価とともに寿命が始まった非volatileオブジェクトを参照する非volatileのリテラル型のlvalueに対して適用されたものを除く
-
ポリモーフィックなクラス型のglvalueをオペランドに指定したtypeid式
-
new式
-
delete式
-
結果が未規定となる比較演算子と等号、不等号演算子
-
throw式
式が整数型かスコープなしenum型で、暗黙にprvalueに型変換でき、型変換した式がコア定数式であるような式を、整数定数式(integral constant expression)という。
このような整数定数式は、配列の宣言の添字や、ビットフィールドや、enum初期化子や、アライメントなどに使える。
T型に変換された定数式(converted constant expression)とは、暗黙にT型のprvalueに変換された式のことで、その変換された式がコア定数式で、暗黙の型変換には、ユーザー定義型変換、lvalueからrvalueへの変換、整数のプロモーション、ナロー変換以外の整数の型変換だけが使われているものを言う。
変換された定数式は、new式、case式、内部型が固定されたenum初期化子、配列の範囲など、整数かenumの非型テンプレート実引数など、型変換が必要な定数式の文脈で使われる。
ここまで解説して、始めて定数式の定義ができるようになる。
定数式(constant expression)とはコア定数式のうち、glvalueのstaticストレージか、関数か、prvalueコア定数式である。
prvalueコア定数式の場合、その値のオブジェクトとサブオブジェクトが、以下の条件をすべて満たす場合のみ、定数式になる。
-
リファレンス型の非staticデータメンバーはすべて、staticストレージ上のオブジェクトか、関数を参照していること
-
オブジェクトかサブオブジェクトがポインター型の場合、その値は、staticストレージ上のオブジェクトへのアドレスか、staticストレージ上のオブジェクトを超えたアドレスか、関数のアドレスか、nullポインターであること
C++規格は、浮動小数点数の計算精度を規定していないため、C++の実装でコンパイル時と実行時で、浮動小数点数の計算結果が変わったとしても、その実装は規格準拠である。
bool f()
{
char array[ 1 + int( 1 + 0.2 - 0.1 - 0.1 ) ] ; // コンパイル時評価される
int size = 1 + int( 1 + 0.2 - 0.1 - 0.1 ) ; // 実行時評価される可能性がある
return sizeof( array ) == size ;
}
このような関数fの戻り値がtrueかfalseか、C++規格は規定することができない。
リテラルクラス型の式が、整数定数式を必要とする文脈で用いられた場合、その式は文脈上、整数型かスコープなしenum型に暗黙に変換される。その場合に使われる変換関数は、constexprでなければならない。
識別子 : 文
case 定数式 : 文
default : 文
文にはラベルを付けることができる。ラベルとは、その文を指す識別子である。文にラベルを付けるための文を、ラベル文(label statement)という。ラベル文には、必ず後続する文が存在しなければならない。
void f()
{
label :
// エラー、ラベルに続く文がない
} // ブロックの終わり
void g()
{
// OK、ラベルに続く文がある
label_1 : /* 式文 */ ;
label_2 : { /* 複合文 */ } ;
}
識別子ラベル(identifier label)は、識別子を宣言する。この識別子は、goto文でしか使えない。
int main()
{
label_1 : ;
label_2 : ;
label_3 : ;
goto label_2 ;
}
ラベルの識別子のスコープは、宣言された関数内である。ラベルを再宣言することはできない。ラベルの識別子は、宣言する前にgoto文で使うことができる。
// ラベルのスコープは、宣言された関数内
void f() { label_f : ; }
void g()
{
goto label_f ; // エラー、この関数のスコープのラベルではない
}
// ラベルを再宣言することはできない
void h()
{
label_h : ; // label_hの宣言
label_h : ; // エラー、ラベルの再宣言はできない
}
// ラベルの識別子は、宣言する前にgoto文で使うことができる
void i()
{
// 識別子label_iは、この時点では、まだ宣言されていないが、使うことができる
goto label_i ;
label_i ;
}
ラベルの識別子は、独自の名前空間を持つので、他の識別子と混同されることはない。
int main()
{
identifier : ; // ラベルの識別子
int identifier ; // 変数の識別子
goto identifier ; // ラベルの識別子が使われる
identifier = 0 ; // 変数の識別子が使われる
}
caseラベルとdefaultラベルは、switch文の中でしか使うことができない。
int main()
{
switch(0)
{
case 0 : ;
default : ;
}
}
式opt ;
式文(expression statement)とは、式を書く事のできる文である。文の多くは、この式文に該当する。式文は、セミコロン(;)を終端記号として用いる。式文は、書かれている式を評価する。
int main()
{
0 ; // 式は0
1 + 1 ; // 式は1 + 1
// これは式文ではなく、return文
return 0 ;
}
式文は、式を省略することもできる。式を省略した式文を、null文という。
/* 式を省略*/ ; // null文
;;;; // null文が四つ
;;;;;;;; // null文が八つ
null文は、評価すべき式がないので、何もしない文である。null文はたとえば、ブロックの終りにラベル文を書きたい場合や、for文やwhile文のようなループを、単に回したい場合などに、使うことができる。
int main()
{
// 単にループを回すだけのfor文
for ( int i = 0 ; i != 10 ; ++i ) ;
label : ; // ラベル文には、後続する文が必要。
}
{ ひとつ以上の文opt }
複合文、またはブロックという文は、文をひとつしか書けない場所に、複数の文を書くことができる文である。
void f( bool b )
{
if ( b )
/*ここにはひとつの文しか書けない*/ ;
if ( b )
{
// いくらでも好きなだけ文を書くことができる。
}
}
複合文は、ブロックスコープを定義する。
int main()
{ // ブロックスコープ
{ // 新たなブロックスコープ
}
}
選択文は、複数あるフローのうち、どれかひとつを選ぶ文のことである。
もし、選択文の中の文が、複合文ではなかった場合、その文を複合文で囲んだ場合と同じになる。
void f( bool b )
{
if ( b )
int x ;
x = 0 ; // エラー、xは宣言されていない
}
このコードは、以下のコードと同等であるため、if文の次の式文で、xという名前を見つけられない。
void f( bool b )
{
if ( b )
{ int x ; }
x = 0 ; // エラー、xは宣言されていない
}
条件について
条件:
式
宣言
条件には、式か宣言を書くことができる。条件は、if文やswitch文だけではなく、while文などでも使われる。
void f()
{
if ( true ) ;
if ( int x = 1 ) ;
}
条件に宣言を書くことができる理由は、コードを単純にするためである。
// 何か処理をして結果を返す関数
int do_something() ;
int main()
{
int result = do_something() ;
if ( result )
{
// 処理
}
}
条件には宣言を書く事ができるため、以下のように書くことができる。
if ( int result = do_something() )
if ( 条件 ) 文
if ( 条件 ) 文 else 文
if文は、条件の値によって、実行すべき文を変える。
条件がtrueと評価された場合、一つ目の文が実行される。条件がfalseと評価された場合、elseに続く二つ目の文が有るのならば、二つ目の文が実行される。
int main()
{
if ( true ) // 一つ目の文が実行される
/*一つ目の文*/ ;
if ( false ) // 一つ目の文は実行されない
/*一つ目の文*/ ;
if ( true ) // 一つ目の文が実行される
/*一つ目の文*/ ;
else
/*二つ目の文*/ ;
if ( false ) // 二つ目の文が実行される
/*一つ目の文*/ ;
else
/*二つ目の文*/ ;
}
elseは、近い方のif文に対応する。
int main()
{
if ( false ) // #1
if ( true ) ; // #2
else { } // #2のif文に対応するelse
}
インデントに騙されてはいけない。インデントを正しく対応させると、以下のようになる。
int main()
{
if ( false ) // #1
if ( true ) ; // #2
else ; // #2のif文に対応するelse
}
このため、elseのあるif文の中に、さらにif文をネストさせたい場合は、内側のif文にも、elseが必要である。
int main()
{
if ( false ) // #1
if ( true ) ; // #2
else ; // #2のif文に対応するelse
else ; // #1のif文に対応するelse
}
あるいは、ブロック文を使うという手もある。
int main()
{
if ( false ) // #1
{ if ( true ) ; }
else ; // #1のif文に対応するelse
}
switch( 条件 ) 文
switch文は、条件の値によって、実行する文を選択する。
条件は、整数型かenum型、もしくは非explicitな変換関数を持つクラス型でなければならない。条件がクラス型の場合、整数型かenum型に型変換される。
struct C
{
operator int(){ return 0 ; }
} ;
int main()
{
switch(1) ; // OK
C c ;
switch(c) ; // OK、C::operator int()が呼ばれる
switch(1.0) ; // エラー、浮動小数点数型は指定できない
switch( static_cast<int>(1.0) ) ; // OK
}
switch文の中の文には、通常、複合文を指定する。複合文の中には、caseラベル文やdefaultラベル文を書く。
switch(1)
{
case 1 :
/* 処理 */ ;
break ;
case 2 :
/* 処理 */ ;
break ;
default :
/* 処理 */ ;
}
caseラベル文に指定する式は、整数の定数式でなければならない。また、同じswitch内で、caseラベル文の値が重複してはならない。
defaultラベル文は、switch文の中の文に、ひとつだけ書くことができる。
switch文が実行されると、まず条件が評価される。結果の値が、switch文の中にあるcaseラベルに対して、ひとつづつ比較される。もし、値が等しいcaseラベル文が見つかった場合、そのラベル文に実行が移る。
void f( int const value )
{
switch( value )
{
case 1 :
std::cout << "Good morning." << std::endl ;
break ;
case 2 :
std::cout << "Good afternoon." << std::endl ;
break ;
case 3 :
std::cout << "Good evening." << std::endl ;
break ;
}
}
int main()
{
f( 1 ) ; // Good morning.
f( 2 ) ; // Good afternoon.
f( 3 ) ; // Good evening.
}
条件と値の等しいcaseラベルが見つからない場合で、defaultラベルがある場合、defaultラベルに実行が移る。
void f( bool const value )
{
switch( value )
{
case true :
std::cout << "true" << std::endl ;
break ;
default :
std::cout << "false" << std::endl ;
break ;
}
}
int main()
{
f( true ) ; // true
f( false ) ; // false
}
条件と値の等しいcaseラベルが見つからず、defaultラベルもない場合、switch内の文は実行されない。
int main()
{
// switch内の文は実行されない
switch( 0 )
{
case 999 :
std::cout << "hello" << std::endl ;
break ;
case 123456 :
std::cout << "hello" << std::endl ;
break ;
}
}
caseラベルとdefaultラベル自体には、文の実行を変更する機能はない。
void f( int const value )
{
switch( value )
{
case 1 :
std::cout << "one" << std::endl ;
default :
std::cout << "default" << std::endl ;
case 2 :
std::cout << "two" << std::endl ;
}
}
この場合、valueの値が1の場合、case 1のラベル文に続く文も、すべて実行されてしまう。また、valueの値が1でも2でもない場合、defaultラベル文に続くcase 2のラベル文も、実行されてしまう。このため、switch内の実行を切り上げたい時点で、break文を書かなければならない。break文を書き忘れたことによる、意図しない文の実行は、よくあることなので、注意が必要である。なお、このことは、逆に利用することもできる。
void f( int const value )
{
switch( value )
{
case 3 :
case 5 :
case 7 :
/* 何らかの処理 */ ;
}
}
この例では、valueの値が3, 5, 7のいずれかの場合に、何らかの処理が実行される。
繰り返し文(Iteration statements)は、ループを書くための文である。
繰り返し文の中の文は、暗黙的に、ブロックスコープを定義する。このブロックスコープは、文の実行のループ一回ごとに、出入りする。例えば、
while ( true )
int i ;
という文は、以下のように書いたものとみなされる。
while ( true )
{ int i ; }
従って、繰り返し文の中の変数は、ループが回されるごとに、生成、破棄されることになる。
struct C
{
C(){ std::cout << "constructed." << std::endl ; }
~C(){ std::cout << "destructed." << std::endl ; }
} ;
int main()
{
while ( true )
{ // 生成、破棄を繰り返す
C c ;
}
}
while ( 条件 ) 文
while文は、条件の結果がfalseになるまで、文を繰り返し実行する。条件は、文の実行前に、繰り返し評価される。
int main()
{
// 一度も繰り返さない
while ( false )
{
std::cout << "hello" << std::endl ;
}
// 無限ループ
while ( true )
{
std::cout << "hello" << std::endl ;
}
// iが10になるまで繰り返す
int i = 0 ;
while ( i != 10 )
{
++i ;
}
}
条件が宣言である場合、変数のスコープは、while文の宣言された場所から、while文の最後までである。条件の中で宣言された変数は、文の実行が繰り返されるたびに、生成、破棄される。
while ( T t = x ) 文
という文は、
label:
{
T t = x;
if (t)
{
文
goto label;
}
}
と書くのに等しい。
while文の条件の中で宣言された変数は、ループの繰り返しのたびに、破棄されてから再び生成される。
#include <iostream>
class nonzero
{
private :
int value ;
public :
nonzero( int i )
: value(i)
{ std::cout << "constructed" << std::endl ; }
~nonzero()
{ std::cout << "destructed" << std::endl ;}
operator bool() { return value != 0 ; }
} ;
int main()
{
int i = 3 ;
while ( nonzero n = i )
{
--i ;
}
}
do 文 while ( 式 ) ;
do文の式は、boolに変換される。boolに変換できない場合、エラーとなる。
do文は、式の結果がfalseになるまで、文が繰り返し実行される。ただし、式の評価は、文の実行の後に行われる。
int main()
{
// 一度だけ文を実行
do {
std::cout << "hello" << std::endl ;
} while ( false ) ;
// 無限ループ
do {
std::cout << "hello" << std::endl ;
} while ( true ) ;
}
for ( for初期化文 条件opt ; 式opt ) 文
for初期化文:
式文
宣言
for文は、for初期化文で、ループ前の初期化を書き、条件で、ループを実行するかどうかの判定を行い、文が実行されたあとに、そのつど式が評価される。
for文の実行では、まず、for初期化文が実行される。for初期化文は、式文か、変数の宣言を行うことができる。変数のスコープは、for文の最後までである。次に、文の実行の前に、条件が評価され、falseとなるまで文が繰り返し実行される。文の実行の後に、式が評価される。
for ( for初期化文 条件 ; 式 ) 文
は、以下のコードと同等である。
{
for初期化文
while ( 条件 )
{
文
式 ;
}
}
ただし、文の中でcontinue文を使ったとしても、式は評価されるという違いがある。
for文は、while文でよく書かれるコードを書きやすくした構文である。例えば、while文を10回実行したい場合、
int main()
{
// カウンター用の変数の宣言
int i = 0 ;
while ( i != 10 )
{
// 処理
++i ;
}
}
このようなコードを書く。for文は、このようなコードを、一度に書けるようにしたものである。
int main()
{
for ( int i = 0 ; i != 10 ; ++i )
{
// 処理
}
}
for文の条件と式は、省略することができる。条件を省略した場合、trueとみなされる。
int main()
{
// 条件を省略、for ( ; true ; ) と同じ
for ( ; ; ) ;
}
ここでは、range-based forの言語機能を説明している。ライブラリとしてのレンジや、ユーザー定義のクラスでレンジをサポートする方法については、ライブラリのレンジを参照。
for ( for-range-宣言 : for-range-初期化子 ) 文
range-based forは、レンジをサポートしている配列、初期化リスト、クラスの各要素に対して、それぞれ文を実行するための文である。
range-based forは、forに続けて、括弧を書く。括弧の中には、変数の宣言と、レンジとを、:で区切る。
int main()
{
int a[] = { 1, 2, 3 } ;
for ( int i : a ) ; // 各要素をint型のコピーで受ける
for ( int & ref : a ) ; // 各要素をリファレンスで受ける
for ( auto i : a ) ; // auto指定子を使った例
}
このようにして宣言した変数は、range-based for文の中で使うことができる。range-based for文は、変数をレンジの各要素で初期化する。
int main()
{
int a[] = { 1, 2, 3 } ;
for ( auto i : a )
{
i ;
}
}
この例では、ループは3回実行され、変数iの値は、それぞれ、1, 2, 3となる。
ループを使ってコードを書く場合、配列やコンテナーの各要素に対して、それぞれ何らかの処理をするという事が多い。
#include <iostream>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 } ;
for (
int * iter = &a ; // 各要素を表す変数の宣言
iter != &a + 5 ; // 終了条件の判定
++iter // 次の要素の参照
)
{
// 各要素に対する処理
std::cout << *iter << std::endl ;
}
}
しかし、このようなループを正しく書くのは、至難の業である。なぜならば、人間は間違いを犯すからである。しかし、このようなループは、誰が書いても、概ね似たようなコードになる。range-based forを使えば、このような冗長なコードを省くことができる。
int main()
{
int a[5] = { 1, 2, 3, 4, 5 } ;
for ( auto i : a )
{
std::cout << i << std::endl ;
}
}
range-based forは、極めて簡単に使うことができる。for-range-宣言で、各要素を得るための変数を宣言する。for-range初期化子で、レンジをサポートした式を書く。文で、各要素に対する処理を書く。
int main()
{
int a[5] = { 1, 2, 3, 4, 5 } ;
for ( int & i : a )
{
i *= 2 ; // 二倍する
}
}
この例では、配列aの各要素は、二倍される。配列の要素を書き換えるために、変数は参照で受けている。
range-based forには、配列の他にも、初期化リストや、レンジをサポートしたクラスを書く事ができる。STLのコンテナーは、レンジをサポートしている。配列以外にrange-based forを適用する場合、<iterator>の#includeが必要である。
#include <iterator>
int main()
{
// 配列
int a[] = { 1, 2, 3 } ;
for ( auto i : a )
{ std::cout << i << std::endl ; }
// 初期化リスト
for ( auto i : { 1, 2, 3 } )
{ std::cout << i << std::endl ; }
// クラス
std::vector<int> v = { 1, 2, 3 } ;
for ( auto i : v )
{ std::cout << i << std::endl ; }
}
range-based forは、本来、コンセプトという言語機能と共に提供される予定であった。しかし、コンセプトは紆余曲折を経た結果、C++11では却下された。そのため、現行のrange-based forは、コンセプトではなく、ADLによる実装をされている。
以下のrange-based for文があるとする。
for ( for-range-宣言 : for-range-初期化子 ) 文
このrange-based for文は、以下のように変換される。
for-range-初期化子が式の場合、括弧でくくられる。これは、コンマ式が渡されたときに、正しく式を評価するためである。
for ( auto i : a, b, c, d ) ;
// 括弧でくくる
for ( auto i : (a, b, c, d) ) ;
for-range-初期化子が初期化リストの場合、なにもしない。
{
// 式の結果をlvalueかrvalueのリファレンスで束縛
auto && __range = for-range-初期化子 ;
for (
auto __begin = begin式, // 先頭のイテレーター
__end = end式 ; // 終端のイテレーター
__begin != __end ; // 終了条件
++__begin ) // イテレーターのインクリメント
{
for-range-宣言 = *__begin; // 要素を得る
文
}
}
ここでの、__range、__begin、__endという変数は、説明のための仮の名前である。実際のrange-based for文の中では、このような変数名は存在しない。
__rangeとは、for-range-初期化子の式の結果を保持するためのリファレンスである。auto指定子とrvalueリファレンスの宣言子が使われていることにより、式のlvalue、rvalue、CV修飾子をいかんを問わずに、結果をリファレンスとして束縛できる。
begin式とend式は、先頭と終端へのイテレーターを得るための式である。
for-range-初期化子の型が、配列の場合、begin式は「__range」となり、end式は、「__range + 配列の要素数」となる。
int x [10] ;
for ( auto i : x )
{
// 処理
}
上記のrange-based for文は、以下のように変換される。
int x [10] ;
{
auto && __range = ( x ) ;
for (
auto __begin = __range,
__end = __range + 10 ;
__begin != __end ;
++__begin )
{
auto i = *__begin;
// 処理
}
}
型が配列以外の場合、begin式は「begin(__range)」に、end式は「end(__range)」に変換される。
std::vector<int> v ;
for ( auto i : v )
{
// 処理
}
std::vector<int> v ;
{
// 式の結果をlvalueかrvalueのリファレンスで束縛
auto && __range = ( v ) ;
for (
auto __begin = begin(__range),
__end = end(__range) ;
__begin != __end ;
++__begin )
{
auto i = *__begin;
// 処理
}
}
ここでのbegin(__range)とend(__range)は、関数呼び出しである。ただし、この名前の解決には、通常の名前探索のルールは用いられない。begin/endの名前探索には、関連名前空間に特別にstdを加えた、ADLによってのみ名前探索される。通常のunqualified名前探索は用いられない。ADLの詳細については、詳しい説明を別に設けてあるので、そちらを参照。
ジャンプ文は、実行する文を無条件で変更するための文である。
break ;
break文は、繰り返し文かswitch文の中で使うことができる。break文は、最も内側の繰り返し文かswitch文から、抜け出す機能を持つ。もし繰り返し文かswitch文に続く、次の文があれば、実行はその文に移る。break文は、ループを途中で抜けたい場合に使うことができる。
int main()
{
while ( true )
{
break ;
}
do
{
break ;
} while ( true ) ;
for ( ; ; )
{
break ;
}
switch(0)
{
default :
break ;
}
}
break文によって抜ける繰り返し文かswitch文とは、break文が書かれている場所からみて、最も内側の文である。
int main()
{
while ( true ) // 外側
while ( true ) // 内側
{
break ;
}
}
break文が使われている内側の文からは抜けるが、外側の文から抜けることはできない。
continue ;
continue文は、繰り返し文の中で使うことができる。continue文を実行すると、そのループの実行を中止する。
while文やdo文の場合、条件が評価され、その結果次第で、次のループが再び始まる。for文の場合は、ループの最後に必ず行われる式が、もしあれば評価され、条件が評価され、その結果次第で、次のループが再び始まる。
int main()
{
while ( true )
{
continue ;
}
do
{
continue ;
} while ( true ) ;
for ( int i = 0 ; true ; ++i )
{
continue ; // for文の式である++iが評価される。
}
}
continue文に対する繰り返し文とは、continue文が書かれている場所からみて、最も内側の繰り返し文のループである。
int main()
{
while ( true ) // 外側
while ( true ) // 内側
{
continue ;
}
}
この例では、continue文は、内側のwhile文のループを中止する。ただし、continue文はbreak文とは違い、繰り返し文から抜け出すわけではないので、内側のwhile文の実行が続く。
return 式opt ;
return 初期化リスト ;
return文は、関数の呼び出し元に実行を戻す文である。
int f()
{
return 0 ; // OK
return ; // エラー、戻り値がない
}
return文の式は、関数の呼び出し元に、戻り値として返される。式は関数の戻り値の型に暗黙的に変換される。変換できない場合はエラーとなる。
戻り値を返さない関数の場合、return文の式は省略できる。戻り値を返さない関数とは、戻り値の型がvoid型の関数、コンストラクター、デストラクターである。
struct C
{
C() { return ; }
~C() { return ; }
} ;
void f() { return ; }
戻り値を返さない関数の場合は、return文で戻り値を返してはならない。
void f()
{
return ; // OK
return 0 ; // エラー、関数fは戻り値を返さない
}
ただし、return文の式がvoidと評価される場合は、戻り値を返していることにはならない。
void f() { }
void g()
{
// 関数fの呼び出しの結果は、void
return f() ;
}
関数の本体の最後は、値を返さないreturn文が書かれたことになる。
void f()
{
// 値を返さないreturn文が書かれた場合と同じ
}
値を返す関数で、return文が省略された場合の挙動は未定義である。ただし、main関数だけは、特別に0が返されたものとみなされる。
// 値を返す関数
int f( bool b )
{
if ( b )
{ return 0 ; }
// bがfalseの場合の挙動は未定義
}
int main()
{
// return 0 ;が書かれた場合と同じ
}
return文には、初期化リストを書くことができる。
std::initializer_list<int> f()
{
return { 1, 2, 3 } ;
}
struct List
{
List( std::initializer_list<int> ) { }
} ;
List g()
{
return { 1, 2, 3 } ;
}
return文は、関数の戻り値の為に、一時オブジェクトを生成するかもしれない。一時オブジェクトを生成する場合、値はコピーかムーブをしなければならないが、return文では、コピーかムーブかの選択のために、式をrvalueとみなす可能性もある。式をrvalueとみなすということは、lvalueであっても、暗黙的にムーブされる可能性があることを意味する。これは例えば、「return文を実行して関数の呼び出し元に戻った場合、関数のローカル変数は破棄されるためムーブしてもかまわない」という状況で、コピーではなく、ムーブを選択できるようにするためである。
// コピーとムーブが可能なクラス
struct C
{
C() = default ; // デフォルトコンストラクター
C( C const & ) = default ; // コピーコンストラクター
C( C && ) = default ; // ムーブコンストラクター
} ;
C f()
{
C c ;
// 一時オブジェクトが生成される場合、コピーかムーブが行われる。
return c ;
// なぜならば、ローカル変数はreturn文の実行後、破棄されるので、ムーブしても構わないからである。
}
また、上記のコードで、一時オブジェクトが生成されない場合もある。これはインライン展開やフロー解析などによる最適化の結果、コピーもムーブも行わなくてもよいと判断できる場合、そのような最適化を許可するためである。
goto 識別子 ;
goto文は、関数内のラベル文に無条件で実行を移すための文である。同じ関数内であれば、どこにでもジャンプできる。
int main()
{
label : ; // labelという名前のラベル文
goto label ;
}
宣言文の前にジャンプする、あるいは、宣言文を飛び越すことについては、宣言文の項目で詳しく解説している。
ブロック宣言 ;
宣言文は、あるブロックの中に、新しい識別子を導入するための文である。ブロック宣言や、その他の宣言についての詳細は、宣言、宣言子、クラスを参照。
int main()
{
int a ; // int型の識別子aという変数の宣言
void f(void) ; // void (void)型の識別子fという関数の宣言
}
自動ストレージの有効期間を持つ変数は、宣言文が実行されるたびに、初期化される。また、宣言されているブロックから抜ける際に、破棄される。
struct Object
{
Object()
{ std::cout << "constructed." << std::endl ; }
~Object()
{ std::cout << "destructed." << std::endl ; }
} ;
int main()
{
{
Object object ; // 生成
} // ブロックスコープから抜ける際に破棄される
}
ジャンプ文を使えば、宣言文の後から前に実行を移すことが可能である。その場合、宣言文によって生成されたオブジェクトは破棄され、宣言文の実行と共に、再び生成、初期化される。
struct Object
{
Object()
{ std::cout << "constructed." << std::endl ; }
~Object()
{ std::cout << "destructed." << std::endl ; }
} ;
int main()
{
label :
Object object ; // 変数objectが生成、初期化される
goto label ; // 変数objectは破棄される
}
この例では、Objectクラスの変数objectは、gotoで宣言文の前にジャンプするたびに、破棄されることになる。
goto文やswitch文などのジャンプ文を使えば、自動変数の宣言文を実行せずに、通り越すコードが書ける。
// goto文の例
void f()
{
// labelという名前のラベル文にジャンプする
goto label ;
int value ; // 自動変数の宣言文
label : ;
// valueの宣言文を、実行せずに通り越してしまった。
}
// switch文の例
void g( int value )
{
switch ( value )
{
int value ; // 変数の宣言文
// 宣言文を飛び越えてしまっている。
case 0 : break ;
case 1 : break ;
default : break ;
}
}
このようなコードは、ほぼすべての場合、エラーとなるので、書くべきではない。では、変数の宣言文を通り越してもエラーとならない場合は何か。これは、相当の制限を受ける。まず、変数の型は、スカラー型か、trivialなデフォルトコンストラクターとtrivialなデストラクターを持つクラス型でなければならない。また、そのような型にCV修飾子を加えた型と、配列型でもよい。その上で、初期化子が存在していてはならない。
struct POD { } ;
// trivialではないコンストラクターを持つクラス
struct Object { Object() {} } ;
int main()
{
// 変数の宣言文を飛び越えるgoto文
goto label ;
// エラー
// 変数の型はスカラー型だが、初期化子がある。
int value = 0;
int scalar ; // OK
// エラー
// 変数のクラス型がtrivialではないコンストラクターを持っている
Object object ;
POD pod ; // OK
label : ;
}
すべてのstatic変数とthread_local変数は、他のあらゆる初期化に先立って、ゼロ初期化される。
int main()
{
goto label ;
static int value ; // static変数は必ずゼロ初期化される
label :
// この場合、valueは0であることが保証されている
if ( value == 0 ) ;
}
ブロックスコープ内のstatic変数とthread_local変数は、定数初期化による早期の初期化が行われない場合、宣言に始めて処理が到達した際に、初期化される。
// 定数初期化できない型
struct X
{
int member ;
X( int value ) : member( value ) { }
} ;
void f()
{
// xのゼロ初期化はすでに行われている
static X x(123) ;
// この時点で、xの初期化は完了している。
}
ブロックスコープ内のstatic変数とthread_local変数が定数初期化されている場合、実装は早期に初期化を行なってもかまわない。ただし、行われるという保証はない。
// 定数初期化できる型
struct X
{
int member ;
constexpr X( int value ) : member(value) { }
} ;
// 定数初期化できない型
struct Y
{
int member ;
Y( int value ) : member(value) { }
} ;
int g()
{
goto label ; // 宣言文を飛び越してしまっている。
// constexpr指定子が使われていないことに注意
// xはstatic変数であり、constexprコンストラクターを使っているため、定数初期化である
static X x( 123 ) ;
// constexprコンストラクターを使っていないため、定数初期化ではない
static Y y( 123 ) ;
label :
// xは初期化されているかもしれないし、初期化されていないかもしれない
// yは初期化されていない
// 両方とも、ゼロ初期化は保証されている
}
この例では、関数gのstaticローカル変数xとyの宣言文には、処理が到達しない。そのため、xとyが初期化されている保証はない。ただし、xは定数初期化なので、実装によっては、早期初期化されている可能性がある。ゼロ初期化だけは常に保証されている。
static変数とthread_local変数は、宣言文の実行のたびに初期化されることはない。
int f( int x )
{
// 一回だけ初期化される
// 定数初期化なので、いつ初期化されるかは定められていない
// ただし、ゼロ初期化はすでに行われている
static int value = 1 ;
// この時点で、初期化は完了していることが保証されている
int temp = value ;
value = x ;
return temp ;
}
int main()
{
f(2) ; // 1
f(3) ; // 2
f(4) ; // 3
}
もし、static変数とthread_local変数の初期化が、例外がthrowされたことにより終了した場合は、初期化は未完了だとみなされる。そのような場合、次に宣言文を実行した際に、再び初期化が試みられる。
int flag = 0 ;
struct X
{
X()
{
if ( flag++ == 0 )
throw 0 ;
}
} ;
void f()
{
static X x ;
}
int main()
{
try
{
f() ; // 関数fのstaticローカル変数xの初期化は未完了
}
catch ( ... ) { }
f() ; // 関数fのstaticローカル変数xの初期化完了
}
もし、static変数とthread_local変数の宣言文の初期化が再帰した場合、挙動は未定義である。
int f( int i )
{
static int s = f(2*i); // エラー、初期化が再帰している
return i+1;
}
この例では、static変数sの初期化が終わらなければ、関数fはreturn文を実行できない。しかし、sの初期化は、再帰している。この場合、挙動は未定義である。
関数形式のキャストを用いた式文と、宣言文とは、文法が曖昧になる場合がある。その場合、宣言文だと解釈される。
int main()
{
int x = 0 ;
// 不必要な括弧がついた宣言文? それともキャスト?
int(x) ;
}
この場合、int(x) ;という文は、キャストを含む式文ではなく、宣言文になる。したがって、上記の例は、変数xの再定義となるので、エラーである。
宣言(declaration)とは、名前がどのように解釈されるかを指定するための文法である。
宣言には、ブロック宣言(block-declaration)、関数定義(function-definition)、テンプレート宣言(template-declaration)、明示的インスタンス化(explicit-instantiation)、明示的特殊化(explicit-specialization)、リンケージ指定(linkage-specification)、名前空間定義(namespace-definition)、空宣言(empty-declaration)、アトリビュート宣言(attribute-declaration)がある。
ブロック宣言には、単純宣言(simple-declaration)、名前空間エイリアス定義(namespace-alias-definition)、using宣言(using-declaration)、usingディレクティブ(using-directive)、static_assert宣言(static_assert-declaration)、エイリアス宣言(alias-declaration)、opaque-enum宣言(opaque-enum-declaration)がある。
単純宣言(simple-declaration)は、大きく分けて三つに分割できる。
アトリビュート指定子 指定子 宣言子 ;
変数や関数の宣言などは、この単純宣言で書かれることになる。
単純宣言のアトリビュート指定子は、宣言子のエンティティに属する。詳しくは、アトリビュートを参照。
指定子というのは、intやclass C、typedefなどを指す。指定子は複数指定できる。
宣言子は、変数や関数、型などを、ひとつ宣言する。宣言子も複数指定できる。
// int型の変数xの宣言
int // 指定子
x // 宣言子
;
// int const * const型の変数pの宣言
const int // 指定子
* const p // 宣言子
;
// typeという名前のint型を宣言。
typedef int // 指定子
type // 宣言子
;
宣言子を複数指定できることには、注意が必要である。例えば、ひとつの宣言文で、複数の変数を宣言することもできる。
// int型で、それぞれa, b, c, dという名前の変数を宣言
// 宣言子は4個
int a, b, c, d ;
これは、比較的分かりやすい。しかし、ポインターや配列、関数などという型は、宣言子で指定するので、ひとつの宣言文で、複数の宣言子を使うということは、非常に読みにくいコードを書く事もできてしまうのである。
int * a, b, c[5], (*d)(void) ;
この文は非常に分かりにくい。この文を細かく区切って解説すると、以下のようになる。
int // 指定子
* a, // int *型の変数
b, // int型の変数
c[5], // int [5]型の変数
(*d)(void) // int(*)(void)型の変数、引数を取らずint型の戻り値を返す関数ポインター
;
ひとつの宣言文で複数の宣言子を書くことは避けるべきである。
static_assert ( 定数式 , 文字列リテラル ) ;
static_assert宣言は、条件付きのコンパイルエラーを引き起こすための宣言である。static_assertの定数式はboolに変換される。結果がtrueならば、何もしない。結果がfalseならば、コンパイルエラーを引き起こす。いわば、コンパイル時のassertとして働くのである。結果がfalseの場合、C++のコンパイラーは文字列リテラルをエラーメッセージとして、何らかの方法で表示する。
static_assert( true, "" ) ; // コンパイルが通る
static_assert( false, "" ) ; // コンパイルエラー
// コンパイルエラー
// 何らかの方法で、helloと表示される。
static_assert( false, "hello" ) ;
具体的な利用例としては、今、int型のサイズが4バイトであることを前提としたコードを書きたいとする。このコードは当然ながらポータビリティがない。そこで、int型のサイズが4バイトではない環境では、コンパイルエラーになってほしい。これは、以下のように書ける。
static_assert( sizeof(int) == 4, "sizeof(int) must be 4") ;
sizeof(int)が4ではない環境のC++のコンパイラーでは、このコードはコンパイルエラーになる。また、文字列リテラルが、何らかの方法で表示される。
また別の例では、以下のような関数があるとする。
// 仕様:Derived型はBase型から派生されていること
template < typename Base, typename Derived >
void f( Base base, Derived derived )
{
// 処理
}
この関数では、Derivedという型は、Baseという型から派生されていることを前提とした処理を行う。そこで、もしユーザーがうっかり、そのような要求を満たさない型を渡した場合、エラーになって欲しい。これは、以下のように書ける。
#include <type_traits>
template < typename Base, typename Derived >
void f( Base base, Derived derived )
{
static_assert(
!std::is_same<Base, Derived>::value // 同じ型でなければtrue
&& std::is_base_of<Base, Derived>::value // DerivedがBaseから派生されていればtrue
, "Derived must derive Base.") ;
// 処理
}
struct Base { } ;
struct Derived : Base { } ;
int main()
{
Base b ; Derived d ;
f(b, d) ; // OK
f(b, b) ; // エラー
}
このように、テンプレート引数の型が、あらかじめ定められた要求を満たしていない場合、static_assertを使ってコンパイルエラーにすることもできる。
static_assertの文字列リテラルには、基本ソース文字セットを使うことができる。C++の実装は、基本ソース文字セット以外の文字を、エラーメッセージとして表示する義務がない。我々日本人としては、日本語を使いたいところだが、すべてのコンパイラーに日本語の文字コードのサポートを義務づけるのが現実的ではない。そのため規格では、現実的に最低限保証できる文字しかサポートを義務づけていない。もちろん、コンパイラーがstatic_assertの日本語表示をサポートするのは自由である。しかし、サポートする義務がない以上、static_assertの文字列リテラルに基本ソース文字セット以外の文字を使うのは、ポータビリティ上の問題がある。
// 文字列リテラルが表示されるかどうかは実装依存
static_assert( sizeof(int) == 4, u"このコードはint型のサイズは4であることを前提にしている" ) ;
指定子には、ストレージクラス指定子、関数指定子、typedef指定子、friend指定子、constexpr指定子、型指定子がある。
指定子は、組み合わせて使うことができる場合もある。例えば、typedef指定子と型指定子は、組み合わせて使うことができる。その際、指定子の順番には、意味が無い。以下の2行のコードは、全く同じ意味である。
// int型の別名typeを宣言
// typedefはtypedef指定子、intは型指定子、typeは宣言子
typedef int type ;
int typedef type ;
もちろん、指定子と宣言子は違うので、以下はエラーである。
// エラー、*は宣言子。宣言子の後に指定子を書く事はできない
int * typedef type ;
ストレージクラス指定子には、register、static、thread_local、extern、mutableがある。
ひとつの宣言の中には、ひとつのストレージクラス指定子しか書く事はできない。つまり、ストレージクラス指定子同士は、組み合わせて使うことができない。ただし、thread_localだけは、staticやexternと併用できる。
register指定子を使ってはならない。registerは、変数への最適化のヒントを示す目的で導入された。これは、まだハードウェアが十分に高速でないので、賢いコンパイラを実装できなかった当時としては、意味のある機能であった。しかし、現在では、ハードウェアの性能の向上により、コンパイラーはより複雑で高機能な実装ができるようになり、registerは単に無視されるものとなってしまった。
registerは歴史的理由により存在する。この機能は、現在では互換性のためだけに残されている機能であり、使用を推奨されていない。また、将来的には廃止されるだろう。
thread_local指定子のある変数は、スレッドストレージの有効期間を持つ。すなわち、thread_localが指定された変数は、スレッドごとに別のオブジェクトを持つことになる。
thread_local指定子は、名前空間スコープかブロックスコープの中の変数と、staticデータメンバーに対して適用することができる。ブロックスコープの変数にthread_localが指定された場合は、たとえstatic指定子が書かれていなくても、暗黙的にstaticと指定されたことになる。
正しい例
// グローバル名前空間のスコープ
thread_local int global_variable ;
// 名前の付いている名前空間のスコープ
namespace perfect_cpp
{
thread_local int variable ;
}
// ブロックスコープ
void f()
{
// 以下の3行は、すべてthread_localかつstaticな変数である。
thread_local int a ;
thread_local static int b ;
static thread_local int c ;
}
struct C
{
// 以下の2行は、すべてthread_localなstaticデータメンバーである。
static thread_local int a ;
thread_local static int b ;
} ;
thread_local指定子は、staticデータメンバーにしか指定できないということには、注意を要する。データメンバーがstaticデータメンバーとなるには、static指定子がなければならない。ブロックスコープ内の変数とは違い、暗黙のうちにstaticが指定されたことにはならない。
struct C
{
// エラー、thread_localは非staticデータメンバーには適用できない。
thread_local int a ;
} ;
thread_localが指定された変数に対する、同じ宣言は、すべてthread_local指定されていなければならない。
// 翻訳単位 1
thread_local int value ;
// 翻訳単位 2
extern thread_local int value ;
// 翻訳単位 2
extern int value ; // エラー、thread_localが指定されていない
static指定子には、変数をstatic変数にするという機能と、名前を内部リンケージにするという機能がある。static指定子は、変数と関数と無名unionに指定することができる。ただし、ブロックスコープ内の関数宣言と、関数の仮引数に指定することはできない。
struct C
{
// staticデータメンバー
static int data ;
// staticメンバー関数
static void f() {}
} ;
int main()
{
// 変数、static変数になる
static int x ;
// 無名union、static変数になる
static union { int i ; float f ; } ;
}
static指定子が指定された変数は、静的ストレージの有効期間を持つ。ただし、thread_local指定子も指定されている場合は、スレッドストレージの有効期間を持つ。
クラスのメンバーに対するstatic指定子については、staticメンバーを参照。
static指定子とリンケージの関係については、プログラムとリンケージを参照。
名前空間スコープにおける、リンケージ指定目的でのstaticの使用は、無名名前空間で代用した方がよい。この機能は、C++11で非推奨にされるはずだったが、直前で見直された。理由は、既存のコードを考えると、この機能を将来的に廃止することはできないからである。
// グローバル名前空間スコープ
// 内部リンケージの指定
static int x ;
static void f() {}
// 無名名前空間を使う
namespace
{
int x ;
void f() {}
}
extern指定子には、名前のリンケージを外部リンケージにするという機能と、名前の定義をしないという機能がある。extern指定子は、変数と関数に適用できる。ただし、クラスのメンバーと関数の仮引数には指定できない。
// 変数
extern int i ;
// 関数
extern void f() ;
extern指定子と、宣言と定義の関係については、宣言と定義を参照。
extern指定子とリンケージの関係については、プログラムとリンケージを参照。
テンプレートの明示的なインスタンス化と、リンケージ指定は、externキーワードを使うが、指定子ではない。
mutable指定子は、constでもstaticでもないクラスのデータメンバーに適用することができる。mutable指定子の機能は、クラスのオブジェクトへのconst指定子を、無視できることである。これにより、constなメンバー関数から、データメンバーを変更することができる。
class C
{
private:
mutable int value ;
public :
void f() const
{
// 変更できる
value = 0 ;
}
} ;
int main()
{
C c ;
c.f() ;
}
mutableの機能について詳しくは、CV修飾子も参照。
関数指定子(Function specifier)には、inline、virtual、explicitがある。
inline指定子が書かれた関数宣言は、インライン関数(inline function)を宣言する。inline指定子は、この関数をインライン展開することが望ましいと示すための機能である。ただし、インライン関数だからといって、必ずしもインライン展開されるわけではない。インライン関数ではなくても、インライン展開されることもある。あくまで最適化のヒントに過ぎない。
// インライン関数
inline void f() { }
クラス定義の中の関数定義は、inline指定子がなくても、自動的にinline関数になる。
struct C
{
// 関数定義、インライン関数である
void f() {}
// 関数の宣言、インライン関数である
inline void g() ;
// 関数の宣言、インライン関数ではない
void h() ;
} ;
// 関数C::gの定義
inline void C::g() { }
// 関数C::hの定義
void C::h() { }
インライン指定子は、関数のリンケージに何の影響も与えない。インライン関数のリンケージは、通常の関数と同じである。すなわち、static指定子があれば内部リンケージ持つ。そうでなければ外部リンケージを持つ。
// 外部リンケージを持つ
inline void f() {}
// 内部リンケージを持つ
inline static void g() {}
ただし、インライン関数は、外部リンケージを持っていたとしても、通常の関数とは異なる扱いを受ける。これは、インライン展開の実装を容易にするための制約である。インライン関数は、その関数を使用するすべての翻訳単位で、「定義」されていなければならない。インライン関数の定義は、すべての翻訳単位で、まったく同一でなければならない。
// 翻訳単位 1
// translation_unit_1.cpp
// 外部リンケージを持つインライン関数の定義
inline void f() {}
inline void g() {}
// 翻訳単位 2
// translation_unit_2.cpp
// 宣言だけ
inline void f() ;
// 定義
inline void g() {}
// 関数の宣言
int main()
{
// エラー
// この翻訳単位に関数fの定義がない
f() ;
// OK、定義もある
g() ;
}
これは、テンプレートと同じような制限となっている。そのため、外部リンケージを持つインライン関数は、通常、ヘッダーファイルに書き、必要な翻訳単位で#includeする。まったく同一ということに関して、詳しくは、ODR(One definition rule)を参照。
ただし、翻訳単位に定義があればいいので、呼び出す場所では、宣言だけだとしても、問題はない。
// 宣言
inline void f() ;
int main()
{
// すでに名前fは宣言されていて、この翻訳単位に定義がある
f() ; // OK
}
// 定義
inline void f() {}
virtual指定子は、クラスの非staticメンバー関数に指定することができる。詳しくは、virtual関数を参照。
explicit指定子は、クラス定義内のコンストラクターと変換関数に指定することができる。詳しくは、コンストラクターによる型変換と、変換関数(Conversion functions)を参照。
typedef指定子は、型の別名を宣言するための指定子である。この別名のことを、typedef名(typedef-name)という。typedef名は、型と同じように扱われる。
typedef int integer ;
// これ以降、typedef名integerは、int型とおなじように使える。
integer main()
{
integer x = 0 ;
integer y = x ;
}
typedef名は、エイリアス宣言(alias-declaration)で宣言することもできる。
using 識別子 = 型名 ;
using integer = int ;
エイリアス宣言では、usingキーワードに続く識別子が、typedef名となる。typedef指定子によって宣言されたtypedef名と、エイリアス宣言によって宣言されたtypedef名は、全く同じ意味を持つ。そのため、本書で「typedef名」と記述されている場合、それはtypedef指定子による宣言であろうと、エイリアス宣言による宣言であろうと、等しく適用される。一方、「typedef指定子」と記述されている場合、エイリアス宣言には当てはまらない。
エイリアス宣言の文法は、typedefより分かりやすい。例えば、関数ポインターの別名を宣言したいとする。
// 同じ意味
typedef void (*type)(void) ;
using type = void (*)(void) ;
typedef指定子は、指定子であるので、単純宣言と同じ文法で名前を宣言しなければならない。using宣言は、名前を先に書き、その後に、純粋な型名を書くことができる。
エイリアス宣言とテンプレートについては、テンプレートエイリアスを参照。
typedef指定子は、クラス以外の同じスコープ内で、同じ型のtypedef名を再宣言することができる。
typedef int I ;
typedef int I ; // OK、同じ型の再宣言
typedef short I ; // エラー、型が違う
void f()
{
typedef short I ; // OK、別のスコープなので別の宣言
}
struct Class_Scope
{
typedef int type ;
typedef int type ; // エラー、クラススコープ内では、同じ型でも再宣言できない
} ;
typedef名とconstの関係は、一見して分かりにくい。
typedef int * type ;
// aの型はint const *
const int * a ;
// bの型は、int * const
const type b ;
これは、指定子と宣言子との違いによる。
const int // 指定子
* a // 宣言子
;
const type // 指定子、typeの型は int *
b // 宣言子
;
変数aの場合、const intへのポインター型となる。変数bの場合、const type型となる。typeの型は、int *なので、int *へのconst型となる。そのため、違う型となる。
friend指定子については、friendを参照。
constexpr指定子は、constexprの制約を満たした、変数の定義、関数と関数テンプレートの宣言、staticデータメンバーの宣言に対して指定できる。
constexpr int value = 0 ;
constexpr指定子を使って定義され、定数式で初期化された変数は、定数式として使うことができる。
void f()
{
constexpr std::size_t size = 10 ;
int a[size] ;
}
constexpr指定子を使う変数の型は、リテラル型でなければならない。
struct literal
{
int a ;
} ;
struct non_literal
{
non_literal() { }
} ;
int main()
{
constexpr literal a{} ; // OK
constexpr non_literal b{} ; // エラー
}
コンストラクター以外の関数にconstexpr指定子を記述すると、その関数は、constexpr関数(constexpr function)となる。コンストラクターにconstexpr指定子を記述すると、そのコンストラクターは、constexprコンストラクター(constexpr constructor)となる。constexpr関数とconstexprコンストラクターは暗黙にinlineになる。
constexpr関数の定義は、以下の条件を満たさなければならない。
以下は合法なconstexpr関数の例である。
constexpr int f()
{
return 1 + 1 ;
}
constexpr int g( int x, int y )
{
return x + y + f() ;
}
constexpr int h( unsigned int n )
{
return n == 0 ? 0 : h( n - 11 ) ;
}
以下は、constexpr関数の制約を満たさない誤ったコードの例である。
// エラー、使えない文の使用
constexpr int f( )
{
constexpr int x = 0 ;
return x ;
}
// エラー、使えない文の使用
constexpr int g( bool b )
{
if ( b )
return 1 ;
else
return 2 ;
}
// エラー、return文がふたつ
constexpr int h()
{
return 0 ;
return 0 ;
}
// エラー、戻り値の型がリテラル型ではない
struct S{ S(){ } } ;
constexpr S i()
{
return S() ;
}
C++11のconstexpr関数の制約はとても厳しい。C++14では、この制約は大幅に緩和される。
constexprコンストラクターの定義は、仮引数の型がリテラルでなければならない。関数の本体は、= deleteか、= defaultか、複合文でなければならない。複合文は以下の制約を満たさなければならない。
-
クラスはvirtual基本クラスを持たないこと
-
関数の本体は関数tryブロックではないこと
-
関数の本体の複合文は、以下のいずれかしか含まないこと
-
null文
-
static_assert宣言
-
typedef宣言とエイリアス宣言で、クラスやenumを定義しないもの
-
using宣言
-
usingディレクティブ
-
クラスの非staticデータメンバーと、基本クラスのサブオブジェクトは、すべて初期化されること
-
非staticデータメンバーと基本クラスのサブオブジェクトの初期化に関わるコンストラクターは、constexprコンストラクターであること
-
非staticデータメンバーに指定された初期化句は定数式であること
struct S
{
int member = 0 ; // 定数式であること
constexpr S() { }
} ;
-
コンストラクターの実引数を仮引数の型に変換する際の型変換は、定数式であること
constexprコンストラクターは、ユーザー定義の初期化を記述したリテラル型のクラスを書くことができる。
struct point
{
int x ;
int y ;
constexpr S( int x, int y )
: x(x), y(y)
{ }
} ;
このようなリテラル型のクラスは、constexpr指定子を使った変数で使える。
constexpr S s( 10, 10 ) ;
どのような実引数(無引数関数も含む)を与えても、constexprが定数式にならない場合、エラーとなる。
// OK、定数式になる実引数がある
constexpr int f( bool b )
{
return b ? throw 0 : 0 ;
}
// エラー、絶対に定数式にならない。
constexpr int g( )
{
throw ;
}
constexpr関数テンプレートや、クラステンプレートのconstexprメンバー関数のインスタンス化された特殊化が、constexpr関数の制約を満たさない場合、そのような関数やコンストラクターは、constexpr関数、constexprコンストラクターとはみなされない。
template < typename T >
constexpr T f( T x )
{
return x ;
}
struct non_literal { non_literal(){ } } ;
int main()
{
f( 0 ) ; // OK、constexpr関数
non_literal n ;
f( n ) ; // OK、ただし通常の関数
}
constexpr関数を呼び出した結果の値は、同等だがconstexprではない関数を呼び出した結果の値と等しくなる。
コンストラクターを除く非staticメンバー関数にconstexpr指定子を使うと、そのメンバー関数はconst修飾される。
struct S
{
constexpr int f() ; // constになる
} ;
これは、以下のように書くのと同等である。
constexpr int f() const ;
constexpr指定子は、これ以外には関数の型に影響を与えない。
constexpr非staticメンバー関数を持つクラスは、リテラル型でなければならない。
// OK、リテラル型
struct literal
{
constexpr int f() { return 0 ; }
} ;
// エラー、リテラル型ではない
struct non_literal
{
non_literal() { }
constexpr int f() { return 0 ; }
} ;
constexpr指定子が変数定義に使われた場合、変数はconstになる。変数の型はリテラル型でなければならず、また初期化されなければならない。
constexpr int a = 0 ;
// エラー、初期化されていない
constexpr int b ;
struct non_literal { non_literal() { } } ;
// エラー、リテラル型ではない
constexpr non_literal c{ } ;
初期化にコンストラクター呼び出しが行われる場合、コンストラクター呼び出しは定数式でなければならない。
型指定子(type specifier)には、クラス指定子、enum指定子、単純型指定子(simple-type-specifier)、複雑型指定子(elaborated-type-specifier)、typename指定子、CV修飾子(cv-qualifier)がある。
クラス指定子はクラスで、enum指定子はenumの宣言で、typename指定子は、テンプレートの名前解決を参照。
型指定子は、一部を除いて、宣言の中にひとつだけ書くことができる。組み合わせることのできる型指定子は、以下の通りである。
constは、const以外の型指定子と組み合わせることができる。volatileは、volatile以外の型指定子と組み合わせることができる。
signedとunsignedは、char, short, int, longを後に書くことができる。
shortとlongは、intを後に書くことができる。
longは、doubleを後に書くことができる。longは、longを後に書くことができる。
long double a = 0.0l ;
long long int b = 0 ;
CV修飾子(cv-qualifier)は、指定子の他に、ポインターの宣言子にも使うことができる。CV修飾子には、constとvolatileがある。この二つのキーワードの頭文字をとって、CV修飾子と呼ばれている。CV修飾子付きの変数は、必ず初期化子が必要である。CV修飾子がオブジェクトに与える影響については、基本事項のCV修飾子を参照。
const int a = 0 ;
volatile int b = 0 ;
const volatile int c = 0 ;
const int d ; // エラー、初期化子がない
指定子の始めに述べたように、指定子の順番に意味はないので、const intとint constは、同じ意味となる。
CV修飾子付きの型へのポインターやリファレンスは、必ずしも、CV修飾子付きのオブジェクトを参照する必要はない。ただし、CV修飾子が付いているように振舞う。
int main()
{
int x = 0 ; // 非constなオブジェクト
x = 0 ; // 変更できる
int const & ref = x ; // 参照する
ref = 0 ; // エラー、変更できない
}
単純型指定子には、基本型、クラス名、enum名、typedef名、auto指定子、decltype指定子を使うことができる。
基本型については、基本型を参照。
注意すべきこととしては、signedやunsignedは、単体で使われると、int型だとみなされる。
// signed int
signed a = 0 ;
// unsigned int
unsigned b = 0 ;
また、shortやlongやlong longは、それぞれintが省略されたものとみなされる。
// short int
short a = 0 ;
// long int
long b = 0 ;
// long long int
long long c = 0 ;
複雑型指定子の複雑(Elaborated)というのは、あまりふさわしい訳語ではないが、本書では便宜上、elaboratedに対し、複雑という訳語を使用する。class、struct、union、enumなどのキーワードを使った型指定子を指す。
struct StructName { } ;
class ClassName { } ;
union UnionName { } ;
enum struct EnumName { value } ;
int main()
{
{
// 複雑型指定子
struct StructName a ;
class ClassName b ;
union UnionName c ;
enum EnumName d = EnumName::value ;
}
{
// 単純型指定子
StructName a ;
ClassName b ;
UnionName c ;
EnumName d = EnumName::value ;
}
}
識別子に対するキーワードは、enumにはenumキーワードを、unionにはunionキーワードを、クラスにはstructキーワードかclassキーワードを、すでに行われた宣言と一致して使わなければならない。
class Name { } ;
struct Name a ; // OK、structキーワードでもよい
enum Name b ; // エラー、キーワードが不一致
union Name c ; // エラー、キーワードが不一致
ここでは、変数の宣言に対するauto指定子について説明する。関数の宣言に対するauto指定子については、宣言子の関数を参照。また、new式にも、似たような機能がある。
変数を宣言する際、型指定子にautoキーワードを書くと、変数の型が、初期化子の式から推定される。
auto a = 0 ; // int
auto b = 0l ; // long
auto c = 0.0 ; // double
auto d = 0.0l ; // long double
もちろん、単なるリテラルだけにはとどまらない。およそ初期化子に書ける式ならば、何でも使うことができる。
int f() { return 0 ; }
bool g(int){ return true ; }
char g(double){ return 'a' ; }
int main()
{
auto a = f() ; // int
// もちろん、オーバーロード解決もされる
auto b = g(0) ; // bool
auto c = g(0.0) ; // char
auto d = &f ; // int (*)(void)
}
auto指定子は、冗長な変数の型の指定を省くためにある。というのも、初期化子の型は、コンパイル時に決定できるので、わざわざ変数の型を指定するのは、冗長だからだ。また、変数の型を指定するのが、非常に面倒な場合もある。
#include <vector>
#include <string>
int main()
{
std::vector< std::string > v ;
// 型名が長くて面倒
std::vector< std::string >::iterator iter1 = v.begin() ;
// 簡潔に書ける
auto iter2 = v.begin() ;
}
この場合では、std::vector< std::string >::iterator型の変数を宣言している。auto指定子を使わないと、非常に冗長になってしまう。
template < typename T1, typename T2 > struct Add { } ;
template < typename T1, typename T2 > struct Sub { } ;
template < typename T1, typename T2 >
Add< T1, T2 > operator + ( T1, T2 )
{
typedef Add< T1, T2 > type ;
return type() ;
}
template < typename T1, typename T2 >
Sub< T1, T2 > operator - ( T1, T2 )
{
typedef Sub< T1, T2 > type ;
return type() ;
}
struct A { } ;
struct B { } ;
int main()
{
A a ; B b ;
auto result = a + b - b + (b - a) ;
}
この場合、resultの型を明示的に書こうとすると、以下のようになる。これはとてもではないが、まともに書く事はできない。
Add< Sub< Add< A, B>, B>, Sub< B, A> > result = a + b - b + (b - a) ;
auto指定子による変数の宣言では、変数の型は、関数のテンプレート実引数の推定と同じ方法で推定される。
auto u = expr ;
という式があったとすると、変数uの型は、
template < typename U > void f( U u ) ;
このような関数を、f(expr)と呼び出した場合の、テンプレート仮引数Uと同じ型となる。
ただし、auto指定子では、初期化子が初期化リストであっても、型を推定できるという違いがある。
// std::initializer_list<int>
auto a = { 1, 2, 3 } ;
// std::initializer_list<double>
auto b = { 1.0, 2.0, 3.0 } ;
// エラー、型を推定できない
auto c = { 1, 2.0 } ;
auto d = { } ;
// OK、明示的なキャスト
auto e = std::initializer_list<int>{ 1, 2.0 } ;
auto f = std::initializer_list<int>{ } ;
テンプレートの実引数推定と同じ方法で型を推定するために、配列型は配列の要素へのポインターに、関数型は関数ポインタ―型になってしまう。これには注意が必要である。
void f() { }
int main()
{
int a[1] ;
auto t1 = a ; // int *
auto t2 = f ; // int (*)(void)
}
auto指定子は、他の指定子や、CV修飾子、宣言子と組み合わせることもできる。
int const expr = 0 ; // exprの型はint const
auto a = expr ; // int
auto const b = expr ; // int const
auto const & c = expr ; // int const &
auto const * d = &expr ; // int const *
static auto e = expr ; // static指定子付きのint型の変数
この際の型の決定も、関数のテンプレート実引数の推定と同じルールになる。
宣言子と初期化子の型が合わない場合は、エラーとなる。
auto & x = 0 ; // エラー
この例では、xの型は、リファレンス型であるが、初期化子の型は、リファレンス型ではない。そのため、エラーとなる。
宣言子がrvalueリファレンスの場合、注意を要する。auto指定子の型は、テンプレート実引数の推定と同じ方法で決定されるので、lvalueリファレンスになることもある。
int main()
{
int x = 0 ;
int && r1 = x ; // エラー、rvalueリファレンスをlvalueで初期化できない
auto && r2 = x ; // OK、ただし、r2の型はint &
auto && r3 = std::move(x) ; // OK、r3の型はint &&
}
これは、テンプレート実引数の推定と同じである。
template < typename U >
void f( U && u ) { }
int main()
{
int x = 0 ;
f( x ) ; // Uはint &
f( std::move(x) ) ; // Uはint &&
}
auto指定子で変数を宣言する場合は、必ず初期化子がなければならない。また、宣言しようとしている変数名が、初期化子の中で使われていてはならない。
auto a ; // エラー、初期化子がない
auto b = b ; // エラー、初期化子の中で宣言しようとしている変数名が使われている
初期化子が要素の型Uの初期化リストの場合、autoの型はstd::initializer_list<U>になる。
// std::initializer_list<int>
auto a = { 1, 2, 3 } ;
宣言子が関数の場合、auto指定子を使った宣言は関数になる。
void f() { }
auto (*p)() -> void = &f ;
auto指定子を使って、ひとつの宣言文で複数の変数を宣言することは可能である。その場合、変数の型は、それぞれの宣言子と初期化子から推定される型になる。
auto a = 0, & b = a, * c = &a ;
この例では、aの型はint、bの型はint &、cの型はint *となる。ただし一般に、コードの可読性の問題から、ひとつの宣言文で複数の変数を宣言するのは、避けたほうがよい。
ただし、複数の変数を宣言する場合、autoによって推定される型は、必ず同じでなければならない。
int x = 0 ;
auto a = x, * b = &x ; // OK、autoに対して推定される型は同じ
auto c = 0, d = 0.0 ; // エラー、型が同じではない
従来の、変数が自動ストレージの有効期間を持つということを明示的に指定する意味でのauto指定子は、廃止された。C++11では、autoキーワードを昔の文法で使用した場合、エラーとなる。
auto int x = 0 ; // エラー、C++11には存在しない昔の機能
decltype ( 式 )
decltypeの型は、オペランドの式の型になる。decltype指定子のオペランドの式は、未評価式である。
int main()
{
decltype( 0 ) x ; // xの型はint
decltype( x ) y ; // yの型はint
int const c = 0 ;
decltype( c ) z = 0 ; // zの型はint const
}
decltype指定子の型は、以下のような順序で、条件の合うところで、上から優先的に決定される。
decltype(e)に対して、
-
もし、eが括弧式で囲まれていない、名前かクラスのメンバーアクセスであれば、decltypeの型は、名前eの型になる。
int x ;
// decltype(x)の型はint
decltype(x) t1 ;
class C
{
public :
int value ;
} ;
C c ;
// decltype(c)の型は、class C
decltype(c) t2 ;
// decltype(c.value)の型は、int
decltype(c.value) t3 ;
もし、eが関数呼び出しかオーバーロード演算子の呼び出しであれば、decltypeの型は、関数の戻り値の型になる。この際、括弧式は無視される。eという名前が見つからない場合や、eの名前がオーバーロード関数のセットであった場合、エラーとなる。
decltypeのオペランドは未評価式なので、sizeofなどと同じく、関数が実際に呼ばれることはない。
int f() ;
// decltype( f() )の型は、int
decltype( f() ) t1 ;
// decltype( (f()) )の型は、int
decltype( (f()) ) t2 ;
// エラー、fooという名前は見つからない
decltype( foo ) t3 ;
// エラー、オーバーロード関数のセット
void f(int) ;
void f(short) ;
decltype(f) * ptr ;
-
もし、eがxvalueであれば、eの型をTとした場合、decltype(e)の型は、T &&となる。
int x ;
// typeの型はint &&
using type = decltype( static_cast< int && >(x) ) ;
-
もし、eがlvalueであれば、eの型をTとした場合、decltype(e)の型は、T &となる。
int x ;
// decltype( (x) ) の型は、int &
using type = decltype( (x) ) ;
条件が上から優先されるということに注意。eが括弧で囲まれていない場合は、すでに1.の条件に当てはまるので、この条件には当てはまらない。この条件はeが括弧で囲まれている場合である。
-
上記以外の場合、decltypeの型は、eの型となる。
// decltype(0)の型は、int
decltype(0) t1 ;
// decltype("hello")の型は、char const [6]
decltype("hello") t2 = "hello" ;
eがlvalueで、しかも括弧式で囲まれている場合は、リファレンス型になるということには、注意を要する。
int x = 0 ;
decltype( x ) t1 = x ; // t1の型はint
decltype( (x) ) t2 = x ; // t2の型はint &
decltypeは、他の型指定子や宣言子と併用できる。
int x ;
decltype( x ) * ptr ; // int *
decltype( x ) const & const_ref = x ; // int const &
decltypeは、ネストされた名前の指定子として使用できる。
struct C
{
typedef int type ;
} ;
int main()
{
C c ;
decltype(c)::type x = 0 ; // int
}
decltypeは、基本クラスの指定子、メンバー初期化子として使用できる。
struct Base { } ;
Base base ;
struct Derived
: decltype(base) // decltypeを基本クラスとして指定
{
Derived()
: decltype(base) () // メンバー初期化子
{ }
} ;
decltypeは、疑似デストラクター名として使用できる。
struct C { } ;
int main()
{
C c ;
c.~decltype(c)() ; // 疑似デストラクターの呼び出し
}
enum指定子は、名前付きの定数と型を宣言、定義する。enum(Enumeration)は、歴史的に列挙型と呼ばれている。enum型の名前は、enum名といい、enumが宣言する定数のことを、列挙子(enumerator)と呼ぶ。
// Eはenum名、valueは列挙子
enum E { value = 0 } ;
本書では、enumの機能を四種類に大別して解説する。unscoped enum、scoped enum、enum基底(enum-base)、enum宣言(opaque-enum-declaration)である。
enum指定子:
enum 識別子opt enum基底opt { 列挙子リスト }
enumというキーワードだけで宣言するenumのことを、unscoped enumという。unscoped enumは、弱い型付けのenumを宣言、定義する。enumの定義は、それぞれ別の型を持つ。列挙子リストとは、コンマで区切られた識別子である。各列挙子には、=に続けて定数を指定することで、値を指定できる。これをenumの初期化子という。ただし、列挙子自体はオブジェクトではない。enumは先頭の列挙子に初期化子がない場合、値は0になる。先頭以外の列挙子に初期化子がない場合、そのひとつ前の列挙子の値に、1を加算した値になる。
// a = 0, b = 1, c = 2
enum E { a, b, c } ;
// a = 3, b = 4, c = 5
enum E { a = 3, b = 4 , c = 5 } ;
// a = -5, b = -4, c = -3
enum E { a = -5, b, c } ;
// a = 0, b = 1, c = 0, d = 1
enum E { a, b, c = 0, d } ;
宣言した列挙子は、次の列挙子から使うことができる。
// a = 0, b = 0, c = 5, d = 3
enum E { a, b = a, c = a + 5, d = c - 2 } ;
enum名とunscoped enumの列挙子は、enum指定子があるスコープで宣言される。
enum GlobalEnum { a, b } ;
GlobalEnum e1 = a ;
GlobalEnum e2 = b ;
int main()
{
enum LocalEnum { c, d } ;
LocalEnum e1 = c ;
LocalEnum e2 = d ;
}
unscoped enumによって宣言された列挙子は、整数のプロモーションによって、暗黙的に整数型に変換できる。整数型は、明示的なキャストによって、enum型に変換できる。整数型の値が、enum型の表現できる範囲を超えていた場合の挙動は、未定義である。
enum E { value = 123 } ;
void f()
{
int x = value ; // enum Eからintへの暗黙の型変換
E e1 = 123 ; // エラー、intからenum Eへの暗黙の型変換はできない
E e2 = static_cast<E>(123) ; // intからenum Eへの明示的なキャスト
}
// コマンドの種類を表す定数
enum Command { copy, cut, paste } ;
// コマンドを処理する関数
void process_command( Command id )
{
switch( id )
{
case copy :
// 処理
break ;
case cut :
// 処理
break ;
case paste :
// 処理
break ;
default :
// エラー処理
break ;
}
}
クラスのスコープ内で宣言された列挙子の名前は、クラスのメンバーアクセス演算子(::, ., ->)を使うことによって、参照することができる。
struct C
{
enum { value } ;
// クラススコープのなかでは名前のまま参照できる
void f() { value ; }
} ;
int main()
{
C::value ; // ::による参照
C c ;
c.value ; // .による参照
C * p = &c ;
p->value ; // ->による参照
}
unscoped enumは、識別子を省略することができる。
// 識別子あり
enum E { a } ;
// 識別子なし
enum { b } ;
scoped enumは、強い型付けをするenumである。
enum struct 識別子 enum基底opt { 列挙子リスト } ;
enum class 識別子 enum基底opt { 列挙子リスト } ;
scoped enumは、enum structかenum classという連続した二つのキーワードによって宣言する。enum structとenum classは、全く同じ意味である。どちらを使ってもよい。enumには、クラスのようなアクセス指定はない。scoped enumの識別子は省略できない。列挙子リストの文法は、unscoped enumと変わらない。
enum struct scoped_enum_1 { a, b, c } ;
enum class scoped_enum_2 { a, b, c } ;
scoped enumは、非常に強い型付けを持っている。列挙子は、scoped enumが宣言されているスコープに導入されることはない。かならず、enum名に::演算子をつけて参照しなければならない。
enum struct E { value } ;
value ; // エラー、scoped enumの列挙子は、このように参照できない
E::value ; // OK
このため、同じスコープ内で、同じ名前の列挙子を持つ、複数のscoped enumを宣言することもできる。
void f()
{
// scoped enumの場合
enum struct Foo { value } ;
enum struct Bar { value } ; // OK
Foo::value ; // enum struct Fooのvalue
Bar::value ; // enum struct Barのvalue
}
void g()
{
// unscoped enumの場合
enum Foo { value } ;
enum Bar { value } ; // エラー、すでにvalueは宣言されている。
}
scoped enumの列挙子は、暗黙的に整数型に変換することはできない。明示的にキャストすることはできる。整数型からenumへの変換は、unscoped enumと変わらない。つまり、明示的なキャストが必要である。
enum struct E { value = 123 } ;
int x = E::value ; // エラー、scoped enumの列挙子は暗黙的に変換できない
int y = static_cast<int>( E::value ) ; // OK、明示的なキャスト
E e = static_cast<E>( 123 ) ; // OK、unscoped enumと同じ
ただし、switch文の中のcaseラベルや、非型テンプレート実引数では、scoped enumも使うことができる。
enum struct E { value } ;
template < int N > struct C { } ;
void f( E e )
{
// switch文のcaseラベル
switch( e )
{ case E::value : ; }
// 非型テンプレート実引数
C< E::value > c ;
}
これが許されている理由は、scoped enumの内部的な値は使わないものの、強い型付けがされた一種のユニークな識別子として、scoped enumを使えるようにするためである。
scoped enumは、強い型付けをするenumである。scoped enumは、列挙子の内部的な値は使わないが、単に名前付きの状態を表すことができる変数が欲しい場合、また、たとえ内部的な値を使うにしても、強い型付けによって、些細なバグを未然に防ぎたい場合などに使うことができる。
enum基底:
: 型指定子
enum型は、内部的には単なる整数型である。この内部的な整数型のことを、内部型(underlying type)という。enum基底(enum-base)は、この内部型を指定するための文法である。enum基底の型は、基本クラスの指定とよく似た文法で指定することができる。enum基底の型指定子は、整数型でなければならない。
enum基底が指定されたenum型の内部型は、enum基底の型指定子の型になる。
enum E : int { } ; // 内部型はint
enum struct Foo : int { } ; // 内部型はint
enum struct Bar : unsigned int { } ; // 内部型はunsigned int
enum Error : float { } ; // エラー、enum基底が整数型ではない
enum基底が省略された場合、scoped enumの内部型は、intになる。
enum struct E { } ; // scoped enumのデフォルトの内部型はint
scoped enumで、int型の範囲を超える値の列挙子を使いたい場合は、明示的にenum基底を指定しなければならない。
unscoped enumのenum基底が省略された場合、内部型は明確に定められることがない。
enum E { } ; // 内部型は定められない。
enumの内部型が定められていない場合、内部型は、enumの列挙子をすべて表現できる整数型になる。その際、どの整数型が使われるかは、実装依存である。どの整数型でも、すべての列挙子を表現できない場合は、エラーとなる。
enum宣言は、正式には、opaque-enum-declarationという。これは、定義をしないenumの宣言である。関数や変数が、定義せずに宣言できるように、enumも、定義せずに宣言することができる。
enum 識別子 enum基底 ;
enum struct 識別子 enum基底opt ;
enum class 識別子 enum基底opt ;
unscoped enumの場合は、必ず定義と一致するenum基底を指定しなければならない。scoped enumの場合は、enum基底を省略した場合、内部型はデフォルトのintになる。ただし、安全のためには、enum宣言と対応するenumの定義には、enum基底を明示的に書いておいたほうがよい。
enum struct Foo : unsigned int ; // 内部型はunsigned int
enum class Bar ; // enum基底が省略された場合、内部型はint
enum E1 : int ; // 内部型はint
enum E2 ; // エラー、unscoped enumの宣言では、enum基底を省略してはならない。
enum宣言によって、宣言のみされたenum名は、通常のenumと同じように使用できる。ただし、列挙子を使うことはできない。なぜならば、列挙子は宣言されていないからだ。
列挙子を使うことができないenum宣言に、何の意味があるのか。enum宣言が導入された背景には、ある種の利用方法では、すべての翻訳単位に列挙子が必要ないこともあるのだ。
無駄な定義を省く
// 翻訳単位 1
enum ID : int { /* 自動的に生成される多数の列挙子 */ } ;
// 翻訳単位 2
enum ID : int ; // enum宣言
void f( ID id ) // IDを引数にとる関数
{
int x = id ;
id = static_cast<ID>(0) ;
}
翻訳単位 1で定義されているenumの列挙子は、外部のツールによって、自動的に生成されるものだとしよう。この定義は、かなり頻繁に更新される。もし、翻訳単位 2では、enumの内部的な値が使われ、列挙子という名前付きの定数には、それほど意味が無い場合、この自動的に生成される多数の列挙子は、無駄である。なぜならば、enumが生成されるたびに、たとえ翻訳単位 2のソースコードに変更がなく、再コンパイルが必要ない場合でも、わざわざコンパイルしなおさなければならないからだ。
なぜenumの定義が必要かというと、完全な定義がなければ、enumの内部型を決定できないからである。C++11では、enum基底によって、明示的に内部型を指定できる。これにより、enumを定義せず宣言することができるようになった。
型安全なデータの秘匿
以下のクラスを考える。
// クラスCのヘッダーファイル
// C.h
enum struct ID { /* 自動的に生成される多数の列挙子 */ } ;
class C
{
public :
// 外部に公開するメンバー
private :
ID id ;
} ;
さて、このクラスを、複数の翻訳単位で使いたいとする。このクラスには、データメンバーとしてenum型の変数があるが、これは外部に公開しない。クラスの中の実装の都合上のデータメンバーである。enumの列挙子は、外部ツールで自動的に生成されるものとする。
すると、このヘッダーファイルを#includeしているソースファイルは、enumが自動的に生成されるたびに、再コンパイルしなければならない。しかし、このクラスを使うにあたって、enumの定義は必要ないはずである。この場合にも、enum宣言が役に立つ。
// クラスCのヘッダーファイル
// C.h
enum struct ID : int ;
class C { /* メンバー */ } ;
// クラスCのソースコード
// C.cpp
enum struct ID : int { /* 自動的に生成される多数の列挙子 */ } ;
// メンバーの実装
このようにしておけば、enumの定義が変更されても、クラスのヘッダーファイルを#includeして、クラスを使うだけのソースコードまで、再コンパイルする必要はなくなる。
名前空間とは、宣言の集まりに名前をつける機能である。名前空間の名前は、::演算子によって、宣言を参照するために使うことができる。名前空間は、複数定義することができる。グローバルスコープも、一種の名前空間スコープとして扱われる。
inlineopt namespace 識別子 { 名前空間の本体 }
名前空間は、別の名前空間スコープの中か、グローバルスコープに書くことができる。名前空間の本体には、あらゆる宣言を、いくつでも書くことができる。これには、名前空間自身も含まれる。
// グローバルスコープ
namespace NS
{ // NSという名前の名前空間のスコープ
// 宣言の例
int value = 0 ;
void f() { }
class C { } ;
typedef int type ;
}// NSのスコープ、ここまで
int main()
{
// NS名前空間の中の名前を使う
NS::value = 0 ;
NS::f() ;
NS::C c ;
NS::type t ;
value ; // エラー、名前が見つからない
}
名前空間は、名前の衝突を防ぐために使うことができる。
// グローバルスコープ
int value ;
int value ; // エラー、valueという名前はすでに宣言されている
// OK、名前空間Aのvalue
namespace A { int value ; }
// OK、名前空間Bのvalue
namespace B { int value ; }
int main()
{
value ; // グローバルスコープのvalue
A::value ; // 名前空間Aのvalue
B::value ; // 名前空間Bのvalue
}
グローバル変数として、valueのような、分かりやすくて誰でも使いたがる名前を使うのは、問題が多い。しかし、名前付きの名前空間スコープの中であれば、名前の衝突を恐れずに、気軽に短くてわかりやすい名前を使うことができる。
名前空間は、何度でも定義することができる。
// 最初の定義
namespace NS { int x ; }
// 二度目の定義
namespace NS { int y ; }
名前空間はネストできる。
namespace A { namespace B { namespace C { int value ; } } }
int main()
{
A::B::C::value ; // Aの中の、Bの中の、Cの中のvalue
}
inlineキーワードの書かれた名前空間の定義は、inline名前空間である。inline名前空間スコープの中で宣言された名前は、inline名前空間の外側の名前空間のスコープの中でも使うことができる。
inline namespace NS { int value ; }
namespace Outer
{
inline namespace Inner
{
int value ;
}
}
int main()
{
value ; // NS::value
NS::value ;
Outer::value ; // Outer::Inner::value
Outer::Inner::value ;
}
無名名前空間(unnamed namespace)は、名前をつけない名前空間の宣言である。
namespace { }
無名名前空間は、通常の名前つき名前空間とは違い、その名前空間スコープ内のエンティティを、内部リンケージにするのに使われる。
namespace
{
int x ; // 内部リンケージ
}
ある名前空間スコープの中で宣言された名前を、その名前空間のメンバーと呼ぶ。名前空間のメンバーは、名前空間の外側で定義することができる。
namespace NS
{
void f() ; // 関数fの宣言
namespace Inner
{
void g() ; // 関数gの宣言
void h() ; // 関数hの宣言
}
void Inner::g() { } // 関数gの定義
}
void NS::f() { } // 関数fの定義
void NS::Inner::h() { } // 関数hの定義
ただし、名前空間の外側で定義されているからといって、名前空間の外側のスコープにも、名前が導入されるわけではない。あくまで、名前空間の外側でも、定義ができるだけである。
namespace 識別子 = 名前空間の名前 ;
名前空間エイリアス(Namespace alias)とは、名前空間の名前の別名を定義する機能である。
名前空間は、名前の衝突を防いでくれる。しかし、名前空間の名前自体が衝突してしまうこともある。それを防ぐためには、名前空間の名前には、十分にユニークな名前をつけなければならない。しかし、衝突しない名前をつけようとすると、どうしても、短い名前をつけることはできなくなってしまう。
namespace Perfect_cpp
{
int x ;
}
int main()
{
Perfect_cpp::x = 0 ;
}
この例では、十分にユニークな名前、Perfect_cppを使っている。このため、xという名前の変数名でも、衝突を恐れず使うことができる。しかし、このPerfect_cppは長い上に、大文字とアンダースコアを使っており、タイプしづらい。そこで、名前空間エイリアスを使うと、別名を付けることができる。
namespace Perfect_cpp { int x ; }
int main()
{
namespace p = Perfect_cpp ;
p::x ; // Perfect_cpp::x
}
ネストされた名前にも、短い別名をつけることができる。
namespace Perfect_cpp { namespace Library { int x ; } }
int main()
{
namespace pl = Perfect_cpp::Library ;
pl::x ; // Perfect_cpp::Library::x
}
別名の別名を定義することもできる。
namespace Long_name_is_Looooong { }
namespace long_name = Long_name_is_Looooong ;
namespace ln = long_name ;
同じ宣言領域で、別名と名前空間の名前が衝突してはならない。
namespace A { } namespace B { }
// エラー、同じ宣言領域では、別名と名前空間の名前が衝突してはならない
namespace A = B ;
int main()
{
// OK、別の宣言領域なら可
namespace A = B ;
}
using 識別子 ;
using宣言は、宣言が書かれている宣言領域に、指定した名前を導入する。これにより、明示的に名前空間名と::演算子を使わなくても、その宣言領域で、名前が使えるようになる。
using宣言を、名前空間のメンバーに使う場合、using宣言が書かれているスコープで、::演算子による明示的なスコープの指定なしに、その名前を使うことができる。
namespace NS { int name ; }
int main()
{
name = 0 ; // エラー、名前nameは宣言されていない
using NS::name ;
name = 0 ; // NS::nameと解釈される
NS::name = 0 ; // 明示的なスコープの指定
}
// ブロックスコープ外でもusing宣言は使える
using NS::name ;
using宣言は、テンプレート識別子を指定することはできない。テンプレート名は指定できる。
namespace NS
{
template < typename T > class C { } ;
}
int main()
{
using NS::C ; // OK、テンプレート名は指定できる
using NS::C<int> ; // エラー、テンプレート識別子は指定できない
}
using宣言は、名前空間の名前を指定することはできない。
namespace NS { }
using NS ; // エラー
using宣言は、scoped enumの列挙子を指定することはできない。
namespace NS
{
enum struct E { scoped } ;
enum { unscoped } ;
}
int main()
{
using NS::unscoped ; // OK、unscoped enumの列挙子
using NS::E::scoped ; // エラー、scoped enumの列挙子は指定できない
}
using宣言は、その名の通り、宣言である。したがって、通常通り、外側のスコープの名前を隠すこともできる。usingディレクティブとは、違いがある。
int name ; // グローバルスコープのname
namespace NS { int name ; } // NS名前空間のname
int main()
{
// ここではまだ、名前が隠されてはいない
name = 0 ; // ::nameを意味する
using NS::name ; // このusing宣言は::nameを隠す
name = 0 ; // NS::nameを意味する
}
using宣言は、宣言された時点で、すでに宣言されている名前を、スコープに導入する。宣言場所から見えない名前は、導入されない。
namespace NS { void f( int ) { } }
// void NS::f(int)をグローバルスコープに導入する
// void NS::f(double)は、この時点では宣言されていないので、導入されない
using NS::f ;
namespace NS { void f( double ) { } }
int main()
{
// この時点で、unqualified名fとして名前探索されるのは
// void NS::f(int)のみ
f( 1.23 ) ; // NS::f(int)を呼ぶ。
using NS::f ; // void NS::f(double) をmain関数のブロックスコープに導入する
f( 1.23 ) ; // オーバーロード解決により、NS::f(double)を呼ぶ
}
ただし、テンプレートの部分的特殊化は、プライマリークラステンプレートを経由して探すので、たとえusing宣言の後に宣言されていたとしても、発見される。
namespace NS
{
template < typename T >
class C { } ;
}
using NS::C ;
namespace NS
{
template < typename T >
class C<T * > { } ; // ポインター型への部分的特殊化
}
int main()
{
C<int * > c ; // 部分的特殊化が使われる。
}
using宣言は、コンストラクターの継承に使うこともできる。詳しくは、該当の項目を参照。ここでは、クラスのメンバー宣言としてusing宣言を使う際の、文法上の注意事項だけを説明する。
クラスのメンバー宣言としてusing宣言を使う場合、基本クラスのメンバー名を指定しなければならない。名前空間のメンバーは指定できない。using宣言は、基本クラスのメンバー名を、派生クラスのスコープに導入する。
namespace NS { int value ; }
class Base
{
public :
void f() { }
} ;
class Derived : private Base
{
public :
using Base::f ; // OK、基本クラスのメンバー名
using NS::value ; // エラー、基本クラスのメンバーではない
} ;
クラスのメンバー宣言としてのusing宣言は、基本クラスのメンバーの名前を、クラスのメンバーの名前探索で発見させることができる。
struct Base { void f( int ) { } } ;
struct Derived1 : Base
{
void f( double ) { }
} ;
struct Derived2 : Base
{
using Base::f ;
void f( double ) { }
} ;
int main()
{
Derived1 d1 ;
d1.f( 0 ) ; // Derived::f(double)を呼ぶ
Derived2 d2 ;
d2.f( 0 ) ; // Base::f(int)を呼ぶ
}
Derived1::fは、Base::fを隠してしまうので、Base::fはオーバーロード解決の候補関数に上がることはない。Derived2では、using宣言を使って、Base::fをDerived2のスコープに導入しているので、オーバーロード解決の候補関数として考慮される。
また、using宣言は、基本クラスのメンバーのアクセス指定を変更する目的でも使える。
class Base
{
int value ;
public :
int get() const { return value ; }
void set( int value ) { this->value = value ; }
} ;
// Baseからprivateで派生
class Derived : private Base
{
public : // Base::getのみpublicにする
using Base::get ;
} ;
この例では、DerivedはBaseからprivate派生している。ただし、Base::getだけは、publicにしたい。そのような場合に、using宣言が使える。
using宣言でクラスのメンバー名を指定する場合、クラスのメンバー宣言でなければならない。クラスのメンバー宣言以外の場所で、using宣言にクラスのメンバー名を指定してはならない。
struct C
{
int x ;
static int value ;
} ;
int C::value ;
using C::x ; // エラー、これはクラスのメンバー宣言ではない
using C::value ; // エラー、同上
using namespace 名前空間名 ;
usingディレクティブ(using directive)は、その記述以降のスコープにおける非修飾名前探索に、指定された名前空間内のメンバーを追加するための指示文である。usingディレクティブを使うと、指定された名前空間内のメンバーを、::演算子を用いないで使用できる。
namespace NS
{
int a ;
void f() { }
class C { } ;
}
int main()
{
using namespace NS ;
a = 0 ; // NS::a
f() ; // NS::f
C c ; // NS::C
}
usingディレクティブを使えば、指定された名前空間内のすべてのメンバーを、明示的な::演算子を使わずにアクセスできるようになる。
usingディレクティブは、名前空間スコープとブロックスコープ内で使用することができる。クラススコープ内では使用できない。
namespace A { typedef int type ; }
void f()
{
// ブロックスコープ内
using namespace A ;
type x ; // A::type
}
namespace B
{
// 名前空間スコープ
using namespace A ;
type x ; // A::type
}
other_namespace::type g1 ; // A::type
// 名前空間スコープ(グローバルスコープ)
using namespace A ;
type g2 ; // A::type
class C
{
using namespace A ; // エラー、クラススコープ内では使用できない
} ;
グローバルスコープにusingディレクティブを記述するのは推奨できない。特に、ヘッダーファイルのグローバルスコープにusingディレクティブを記述すると、非常に問題が多い。名前空間の本来の目的は、名前の衝突を防ぐためである。usingディレクティブは、名前空間という仕組みに穴を開けるような機能だからだ。
しかし、usingディレクティブは必要である。たとえば、非常に長い名前空間名や、深くネストした名前空間内の多数のメンバーを使う場合、いちいち::演算子で明示的にスコープを指定したり、using宣言でひとつひとつ宣言していくのは、非常に面倒である。あるブロックスコープで、名前が衝突しないということが保証できるならば、usingディレクティブを使っても構わない。
namespace really_long_name { namespace yet_another_long_name
{
int a ; int b ; int c ; int d ;
} }
void f()
{
// このスコープでは、a, b, c, dという名前は衝突しないと保証できる
using namespace really_long_name::yet_another_long_name ;
a = 0 ; b = 0 ; c = 0 ; d = 0 ;
}
usingディレクティブは、宣言ではない。usingディレクティブは、非修飾名前探索に、名前を探すべき名前空間を、特別に追加するという機能を持っている。したがって、usingディレクティブは、名前を隠さない。以下の例はエラーである。using宣言と比較すると、違いがある。
int name ; // グローバルスコープのname
namespace NS { int name ; } // NS名前空間のname
int main()
{
// ここではまだ、名前が隠されてはいない
name = 0 ; // ::nameを意味する
using namespace NS ; // 名前探索にNS名前空間内のメンバーを追加
name = 0 ; // エラー、::nameとNS::nameとで、どちらを使うべきか曖昧
}
usingディレクティブは、非修飾名前探索にしか影響を与えない。ADLには影響を与えない。
namespace NS
{
struct S { } ;
namespace inner
{
void f(S) { }
void g(S) { }
}
using inner::f ; // inner::fをNS名前空間に導入する
using namespace inner ; // 非修飾名前探索に影響をおぼよす
} ;
int main()
{
NS::S s ;
f(s) ; // OK
g(s) ; // エラー、usingディレクティブはADLには影響しない
}
usingディレクティブで探索できるようになった名前は、オーバーロード関数の候補にもなる。
void f( int ) { }
namespace NS { void f( double ) { } }
int main()
{
// この時点では、NS::fは名前探索で発見されない
f( 1.23 ) ; // ::f(int)
using namespace NS ; // NS名前空間のメンバーがunqualified名前探索で発見されるようになる
f( 1.23 ) ; // オーバーロード解決により、NS::f( double )
}
usingディレクティブは、unqualified名前探索のルールを変更するという、非常に特殊な機能である。usingディレクティブは、確実に名前が衝突しないブロックスコープ内で使うか、あるいは、オーバーロード解決をさせるので、同じ関数名を複数、意図的に名前探索で発見させる場合にのみ、使うべきである。
関数型、外部リンケージを持つ関数名、外部リンケージを持つ変数名には、言語リンケージ(language linkage)という概念がある。リンケージ指定(Linkage specification)は、言語リンケージを指定するための文法である。リンケージ指定と、ストレージクラス指定子のextern指定子とは、別物である。
注意、実装依存の話:言語リンケージは、C++と他のプログラミング言語との間での、関数名や変数名の相互利用のための機能である。異なる言語間で名前を相互利用するには、共通の仕組みが必要である。これには、たとえば名前マングリングを始めとして、レジスターの使い方、引数のスタックへの積み方などの様々な要素がある。しかし、これらはいずれも本書の範疇を超えるので解説しない。
extern 文字列リテラル { 宣言リスト }
extern 文字列リテラル 宣言
標準では、C++言語リンケージと、C言語リンケージを定めている。C++の場合、文字列リテラルは"C++"となり、C言語の場合、文字列リテラルは"C"となる。何も指定しない場合、デフォルトでC++言語リンケージとなる。異なるリンケージ指定がされた名前は、たとえその他のシグネチャーがすべて同じであったとしても、別の型として認識される。その他の文字列がどのような扱いを受けるかは、実装依存である。
// 関数型へのC言語リンケージの指定
extern "C" typedef void function_type() ;
// 関数名へのC言語リンケージの指定
extern "C" int f() ;
// 変数名へのC言語リンケージの指定
extern "C" int value ;
{ }を使う方のリンケージ指定子は、複数の宣言に対して、一括して言語リンケージを指定するための文法である。
// 関数名f, g, hは、すべてC言語リンケージを持つ
extern "C"
{
void f() ;
void g() ;
void h() ;
}
// C_functions.hというヘッダーファイルで宣言されているすべての関数型、関数名、変数名は、C言語リンケージを持つ
extern "C"
{
#include "C_functions.h"
}
リンケージ指定をしない場合、デフォルトでC++言語リンケージだとみなされる。通常、C++言語リンケージを指定する必要はない。
// デフォルトのC++言語リンケージ
void g() ;
// 明示的な指定
extern "C++" void g() ;
リンケージ指定はネストすることができる。その場合、一番内側のリンケージ指定が使われる。言語リンケージは、スコープをつくらない。
extern "C"
{
void f() ; // C言語リンケージ
extern "C++"
{
void g() ; // C++言語リンケージ
}
}
リンケージ指定は、名前空間スコープの中でのみ、使うことができる。ブロックスコープ内などでは使えない。
C言語リンケージは、クラスのメンバーに適用しても無視される。
extern "C"
{
class C
{
void f() ; // C++言語リンケージ
} ;
}
C言語リンケージを持つ同名の関数が、複数あってはならない。これにより、C言語リンケージを持つ関数は、オーバーロードできない。
extern "C"
{
// エラー、C言語リンケージを持つ同名の関数が複数ある
void f(int) ;
void f(double) ;
}
// OK、互いに異なる言語リンケージを持つ
void g() ;
extern "C" void g() ;
このルールは、たとえ関数が名前付きの名前空間の中で宣言されていても、同様である。
namespace A
{
extern "C" void f() ;
}
namespace B
{
extern "C" void f() ; // エラー、A::fとB::fは同じ関数を参照する
void f() ; // OK、C++言語リンケージを持つ
}
このように、たとえ名前空間が違ったとしても、C言語リンケージを持つ関数は、名前が衝突してはならない。これは、名前空間という仕組みが存在しないC言語からでも使えるようにするための仕様である。ただし、C言語リンケージを持つ関数を、C++側から、名前空間の中で宣言して、通常通り使うことはできる。
namespace NS
{
extern "C" void f() { }
}
int main()
{
NS::f() ; // OK
}
これにより、C言語で書かれた関数を、何らかの名前空間の中にいれて、C++側から使うこともできる。
// ヘッダーファイル、C_functions.hは、C言語で書かれているものとする
namespace obsolete_C_lib
{
extern "C"
{
#include "C_functions.h"
}
}
NOTE: アトリビュートの解説は、C++14となる予定の現時点での最新ドラフト、N3797を参考にしている。
アトリビュート(attribute)は、属性とも呼ばれ、ソースコードに追加的な情報を指定するための文法である。
アトリビュートの文法は、以下のようになる。
[[ アトリビュートリスト ]]
アトリビュートは、文法上、実に様々な箇所に書くことができる。アトリビュートリストには、アトリビュート用のトークンを指定することができる。このトークンは、アトリビュートの中だけで通用する識別子であり、予約語ではない。
アトリビュートは、特定のC++実装の独自機能を、独自の文法を使わずに表現するために追加された。また、C++規格でも、アトリビュート用の機能をいくつか定めている。
アライン(align)やアライメント(alignment)とは、ある型のオブジェクトを構築するストレージのアドレスに、制約を指定する機能である。
アライメント指定子は、通常の[[ ]] のようなアトリビュートの文法を使わない。alignasというキーワードが与えられている。当初、アライメントを指定する機能は、アトリビュートの文法を使う予定だったが、アライメント指定のような基本的な機能には、独自のキーワードが与えられるべきだという合意がなされたため、独自のキーワードが与えられた。指定子と名前はついているものの、アライメント指定子が現れることができる箇所の文法は、アトリビュートの文法に基づいている。
アライメント指定子は、変数とクラスのデータメンバーに指定できる。書ける場所は、宣言の前がわかりやすい。
// OK
alignas(16) char buf[16] ;
struct S
{
// OK
alignas(16) char buf[16] ;
} ;
ただし、ビットフィールド、関数の仮引数、例外宣言、registerストレージクラス指定子つきで宣言された変数には指定できない。
struct S
{
// エラー、ビットフィールド
alignas(4) unsigned int data:16 ;
} ;
// エラー、関数の仮引数
void f( alignas(4) int x ) ;
void g()
{
try { }
// エラー、例外宣言
catch ( alignas(4) int x ) { }
}
アライメント指定子は、クラスの宣言や定義に適用することもできる。書ける場所は、クラスキーの後、クラス名の前である。
struct alignas(16) S ;
struct alignas(16) S { } ;
同様に、アライメント指定子は、enumの宣言や定義にも適用することができる。
enum struct alignas(16) E1 : int
{ value } ;
enum alignas(16) E2
{ value } ;
アライメント指定子にエリプシス(...)を適用したものは、パック展開である。
template < typename ... Types >
struct alignas( Types ... ) // パック展開
S { } ;
アライメント指定子には、二種類の形がある。alignas( )の括弧の中身が、コンマで区切られた代入式という形と、コンマで区切られた型という形だ。
// 代入式
alignas( 4, 8, 16 ) char b1[128] ;
// 型
alignas( short, int, long ) char b2[128] ;
アライメント指定子がalignas(代入式)の形の場合、
-
代入式は整数定数式でなければならない
-
定数式が拡張アライメントであり、実装がそのアライメントをその文脈でサポートしている場合は、宣言されたエンティティのアライメントは、指定されたアライメントになる
-
定数式が拡張アライメントであり、実装がそのアライメントをその文脈でサポートしていない場合、エラーとなる
-
定数式がゼロだと評価された場合、アライメント指定子の効果はなくなる
これ以外の場合、エラーとなる。
// OK
constexpr std::size_t f() { return 4 ; }
alignas( f() ) char b1[128] ;
// エラー
int g() { return 4 ; }
alignas( g() ) char b2[128] ;
// 実装がサポートしている場合OK
// 実装がサポートしていなければエラー
alignas( alignof(std::max_align_t) * 2 ) char b3[128] ;
// OK、ただしアライメント指定の効果なし
alignas( 0 ) char b4[128] ;
アライメント指定子が、alignas(型名)の場合、alignas( alignof(形名) )と同等の効果になる。
// alignas( alignof(int) )と同等
alignas( int ) int x ;
ひとつのエンティティに複数のアライメントが指定された場合、最も厳格なアライメント要求になる。
// もし、実装が16のアライメント要求をサポートしている場合
// アライメント要求は16
alignas( 4, 8, 16 ) char b[128] ;
アライメント指定子が、宣言されるエンティティのアライメント要求より緩いアライメントを指定すると、エラーとなる。
// Sの最低のアライメント要求は8
struct alignas(8) S { } ;
// エラー、Sの最低のアライメント要求より緩い
alignas(4) S s ;
あるエンティティの定義である宣言にアライメント指定子がある場合、同じエンティティの定義ではない宣言は、同等のアライメントを指定するか、アライメント指定子なしでなければならない。
struct alignas(8) S { } ;
struct S ; // OK
struct alignas(8) S ; // OK
struct alignas(4) S ; // エラー、同等ではない
struct alignas(16) S ; // エラー、同等ではない
もし、エンティティの宣言のどれかひとつにでもアライメント指定子があった場合、そのエンティティの定義となる宣言にも、同等のアライメントを指定しなければならない。
struct S { } ; // エラー、アライメント指定つきの宣言がある
struct alignas(4) S ;
アトリビュートトークン、noreturnは、関数がreturnしないことを指定する。このアトリビュートは、関数宣言の宣言子idに適用できる。
// この関数は決してreturnしない
[[ noreturn ]] void f()
{
// なぜならば、必ずthrowするからだ
throw 0 ;
}
noreturnは、宣言中に一度しか現れてはならない。noreturnを指定する関数は、その最初に現れる宣言にこのアトリビュートを適用しなければならない。
void f() ; // エラー、最初の宣言だがnoreturnがない
[[ noreturn ]] void f() ;
もし、ある関数が、ある翻訳単位ではnoreturnが指定され、別の翻訳単位では指定されていない場合、エラーとなる。
noreturnを指定した関数からreturnした場合、挙動は未定義である。例外によって抜けるのは問題がない。
クラスとは、ユーザーが定義できる型である。クラスは、クラス指定子(class-specifier)によって定義する。クラス指定子は指定子なので、宣言文の中で使うことができる。したがって、クラスの定義とは、宣言文である。
クラス指定子:
class-key 識別子opt finalopt 基本クラス指定opt { メンバー指定opt }
class-key:
class
struct
union
class A { } ;
struct B { } ;
union C { } ;
// クラスの定義は宣言文なので、変数やポインターなどを同時に宣言できる。
class C { } obj, *ptr ;
クラスの定義は、宣言文である。したがって、終端には必ず、セミコロンを書かなければならない。
struct A { } ; // 宣言文の終端にはセミコロンが必要
struct B { } // エラー、セミコロンがない
クラス指定子は、識別子をクラス名として、宣言されたスコープに導入する。クラス指定子の識別子は省略することができる。その場合、無名クラスの定義となる。
// 無名クラス
class { } a ;
クラス指定子のキーワードには、class、struct、unionがある。このうち、classとstructは、デフォルトのアクセス指定の違い以外は、全く同じである。詳しくは、メンバーのアクセス指定を参照。unionキーワードによって定義されたクラスは、unionとなる。詳しくは、unionを参照。
クラス名は、現れた場所の直後から、スコープに導入される。
// クラス名は直後から使える
struct A { } * ptr = static_cast<A *>( nullptr ) ;
また、クラス名は、そのクラスのスコープ内にも導入される。
struct A
{
// クラス名はクラスのスコープ内にも導入される
typedef A type ;
} ;
クラスは、'}' 記号が現れた場所以降、完全に定義されたものとみなされる。たとえ、メンバー関数が定義されていなくても、クラス自体は、その場所で定義されている。
class A ; // クラスAは不完全型
class A { void f(void) ; } ; // クラスAは定義された
この例では、A::f(void)は定義されていないが、クラスAは、}が現れた場所以降、すでに定義されている。
クラスの宣言で、クラス名の後、基本クラスの前にfinalを記述できる。
struct base_class { } ;
struct class_name final : base_class
{ } ;
finalが指定されたクラスが基本クラス指定子に現れた場合、エラーとなる。つまり、finalが指定されたクラスから派生することはできない。
class non_final_class { } ;
// OK、finalの指定されていないクラスは基本クラスに指定できる
class ok : non_final_class { } ;
class final_class final { } ;
// エラー、finalの指定されているクラスは基本クラスに指定できない
class error : final_class { } ;
完全型のクラスとそのメンバーのオブジェクトは、ゼロではないサイズを持つ。つまり、sizeof演算子は、少なくとも、1以上を返す。
struct Subobject { } ;
struct Object
{
Subobject sub ;
} ;
int main()
{
Object obj ;
sizeof( obj ) ; // 値は実装依存だが、少なくとも1以上
sizeof( obj.sub ) ; // 同上
}
ただし、基本クラスのサブオブジェクトは、内部的にはサイズを持たないかもしれない。これは最適化のために許されている。
struct Base { } ;
struct Derived : Base { } ;
この場合、sizeof(Base)とsizeof(Derived)は、同じ値を返すかもしれない。
トリビアルにコピー可能なクラス(trivially copyable class)とは、クラスのオブジェクトを構成するバイト列の値を、そのままコピーできるクラスのことである。詳しくは、型を参照。
あるクラスがtrivially copyable classとなるには、以下の条件をすべて満たさなければならない。
非trivialな、コピーコンストラクター、ムーブコンストラクター、コピー代入演算子、ムーブ代入演算子を持たないこと。trivialなデストラクターを持っていること。
つまり、これらの特別なメンバー関数を、ユーザー定義してはならない。また、virtual関数やvirtual基本クラスも持つことはできない。trivialの詳しい定義については、クラスオブジェクトのコピーとムーブを参照。
ここで、「持たない」ということは、delete定義によって削除しても構わないということである。
struct A
{
A( A const & ) =delete ;
A( A && ) = delete ;
A & operator = ( A const & ) = delete ;
A & operator = ( A && ) = delete ;
// trivialなデストラクターは、「持って」いなければならない
int x ;
} ;
また、アクセス指定子が異なるメンバーを持っていても構わない。
上記のクラスAは、コピーもムーブもできないクラスであるが、trivially copyable classである。
トリビアルクラス(trivial class)とは、trivially copyable classの条件に加えて、非トリビアルデフォルトコンストラクターを持たないクラスである。
標準レイアウトクラス(standard-layout class)の詳しい説明については、クラスのメンバーを参照。
あるクラスが標準レイアウトクラスとなるためには、以下の条件をすべて満たさなければならない。
標準レイアウトクラスではない非staticメンバーをもたない。また、そのようなクラスへの配列やリファレンスも持たない。
virtual関数とvirtual基本クラスを持たない。
非staticデータメンバーは、すべて同じアクセス指定子である。
// 標準レイアウトクラス
struct S
{
// すべて同じアクセス指定子
int x ;
int y ;
int z ;
// staticデータメンバーは、別のアクセス指定子でもよい
private :
static int data ;
} ;
int S::data ;
標準レイアウトクラスではないクラスを基本クラスに持たない。
基本クラスに非staticデータメンバーがある場合は、クラスは非staticデータメンバーを持たない。クラスが非staticデータメンバーを持つ場合は、基本クラスは非staticデータメンバーを持たない。つまり、クラスとその基本クラス(複数可)の集合の中で、どれかひとつのクラスだけが、非staticなデータメンバーを持つことが許される。
struct Empty { } ;
struct Non_empty { int member ; } ;
// 以下は標準レイアウトクラス
struct A
// 基本クラスに非staticデータメンバーがある場合
: Non_empty
{
// 非staticデータメンバーを持たない
} ;
struct B
// 基本クラスは非staticデータメンバーを持たない
: Empty
{
// 非staticデータメンバーを持つ場合
int data ;
} ;
// 以下は標準レイアウトクラスではない
// 基本クラスもクラスCも非staticデータメンバーを持っている
struct C
: Non_empty
{
int data ;
} ;
最初の非staticデータメンバーと、基本クラスとで、同じ型を使わない。
// 標準レイアウトクラスではない例
struct A { } ;
struct B
// 基本クラス
: A
{
A a ; // 最初の非staticデータメンバー
int data ;
} ;
// 最初の非staticデータメンバーでなければよい
struct C : A
{
int data ;
A a ;
} ;
この制限は、基本クラスとデータメンバーとの間で、アドレスが重複するのを防ぐためである。
struct A { } ;
struct B : A { A a ; } ;
B obj ;
A * p1 = &obj ; // 基本クラスのサブオブジェクトへのアドレス
A * p2 = &obj.a ; // データメンバーへのアドレス
// p1 != p2が保証される
このような場合、もし、クラスBが標準レイアウトクラスであれば、基本クラスのサブオブジェクトへのアドレスと、データメンバーのサブオブジェクトへのアドレスが重複してしまう。つまり、p1とp2が同じ値になってしまう。異なるアドレスを得られるためには、このようなクラスを標準レイアウトクラスにすることはできない。
標準レイアウトクラスのうち、structとclassのキーワードで定義されるクラスを、特に標準レイアウトstructと言う。unionキーワードで定義されるクラスを、特に標準レイアウトunionという。
PODとは、Plain Old Dataの略である。これは、C言語の構造体に相当するクラスである。C++11では、クラスがPODの条件を満たした際に保証される動作を、trivially copyable classと、標準レイアウトクラスの二つの動作に細分化した。そのため、C++11では、特にPODにこだわる必要はない。
クラスがPODとなるためには、trivial classと標準レイアウトクラスの条件を満たし、さらに、PODではないクラスを非staticデータメンバーに持たないという条件が必要になる。
クラス宣言は、クラス名をスコープに導入する。クラス名は外側のスコープの名前を隠す。
もし、クラス名が宣言されたスコープ内で、同名の変数、関数、enumが宣言された場合は、どちらの宣言もスコープに存在することになる。その場合、クラス名を使うには、複雑型指定子を使う以外に方法がなくなる。
void name() { }
class name { } ;
name n ; // エラー、nameは関数名
class name n ; // OK、複雑型指定子
クラスのメンバー指定には、宣言文、関数の定義、using宣言、static_assert宣言、テンプレート宣言、エイリアス宣言を書くことができる。
struct Base { int value ; } ;
struct S : Base
{
int data_member ; // 宣言文
void member_function() { } // 関数の定義
using Base::value ; // using宣言
static_assert( true, "this must not be an error." ) ; // static_assert宣言
template < typename T > struct Inner { } ; // テンプレート宣言
using type = int ; // エイリアス宣言
} ;
このうち、クラスのメンバーとなるのは、データメンバーとメンバー関数、ネストされた型名、列挙子である。
struct S
{
int x ; // データメンバー
void f() { } ; // メンバー関数
using type = int ; // ネストされた型名
enum { id } ; // 列挙子
} ;
データメンバーは、俗にメンバー変数とも呼ばれている。クラス定義内で、変数の宣言文を書くと、データメンバーとなる。
クラスのメンバーを、クラスの定義内で複数回宣言することはできない。ただし、クラス内のクラスとenumに関しては、前方宣言することができる。
class Outer
{
void f() ;
void f() ; // エラー、複数回の宣言
class Inner ; // クラス内クラスの宣言
class Inner { } ; // OK、クラス内クラスの定義
enum struct E : int ; // クラス内enumの宣言
enum struct E : int { id } ; // OK、クラス内enumの定義
} ;
クラスは、}で閉じた所をもって、完全に定義されたとみなされる。たとえ、メンバー関数が定義されていなくても、クラス自体は完全に定義された型となる。
メンバーはコンストラクターで初期化できる。詳しくはコンストラクターを参照。メンバーは初期化子で初期化できる。詳しくは、staticデータメンバーと、基本クラスとデータメンバーの初期化を参照。
struct S
{
S() : x(0) { } // コンストラクター
int x = 0 ; // 初期化子
static int data ;
} ;
int S::data = 0 ; // 初期化子
メンバーは、externやregister指定子と共に宣言することはできない。メンバーをthread_local指定子と共に宣言する場合は、static指定子も指定しなければならない。
struct S
{
extern int a ; // エラー
register int b ; // エラー
thread_local int c ; // エラー
// OK、staticと共に宣言している
static thread_local int d ;
} ;
thread_local int S::d ; // 定義
基本的に、クラス名と同じ名前のメンバーを持つことはできない。これには、一部の例外が存在するが、本書では解説しない。
struct name
{
static int name ; // エラー
void name() ; // エラー
static void name() ; // エラー
using name = int ; // エラー
enum { name } ; // エラー
union { int name } ; // エラー
} ;
unionではないクラスにおいて、同じアクセス指定下にある非staticデータメンバーは、クラスのオブジェクト上で、宣言された順番に確保される。つまり、先に宣言されたデータメンバーは、後に宣言されたデータメンバーよりも、上位のアドレスに配置される。ただし、実装は必要なパディングを差し挟むかもしれないので、後のデータメンバーが、先のデータメンバーの直後に配置されるという保証はない。
struct S
{
public :
// 同じアクセス指定下にある非staticデータメンバー
int x ;
int y ;
} ;
int main()
{
S s ;
int * p1 = &s.x ;
int * p2 = &s.y ;
// この式は、必ずtrueとなる
p1 < p2 ;
// この式がtrueとなる保証はない
p1 + 1 == p2 ;
}
アクセス指定子が異なる非staticデータメンバーの配置に関しては、未規定である。
標準レイアウトstructのオブジェクトへのポインターは、reinterpret_castによって変換された場合、クラスの最初のメンバーへのポインターに変換できる。また、その逆も可能である。
struct S { int x ; } ;
int main()
{
S s ;
S * p1 = &s ;
int * p2 = &s.x ;
// 以下の2式は、trueとなることが保証されている
p1 == reinterpret_cast< S * >( p2 ) ;
p2 == reinterpret_cast< int * >( p1 ) ;
}
レイアウト互換(layout-compatible)という概念がある。まず、同じ型は、レイアウト互換である。
もし、二つの標準レイアウトstructが、同じ数の非staticデータメンバーを持ち、対応する非staticデータメンバーが、それぞれレイアウト互換であったならば、そのクラスは、お互いにレイアウト互換structである。
もし、二つの標準レイアウトunionが、同じ数の非staticデータメンバーを持ち、対応する非staticデータメンバーが、それぞれレイアウト互換であったならば、そのクラスは、お互いにレイアウト互換unionである。
二つの標準レイアウトstructは、レイアウト互換structのメンバーが続く限り、オブジェクト上で共通の表現をしていると保証される。
// A、Bは、2番目のメンバーまで、お互いにレイアウト互換
struct A
{
int x ;
int y ;
float z ;
} ;
struct B
{
int x ;
int y ;
double z ;
} ;
int main()
{
A a ;
B * ptr = reinterpret_cast< B * >( &a ) ;
// OK、aのオブジェクトの、対応するレイアウト互換なメンバーが変更される
ptr->x = 1 ;
ptr->y = 2 ;
// エラー、3番目のメンバーは、レイアウト互換ではない
ptr->z = 0.0 ;
}
ただし、メンバーがビットフィールドの場合、お互いに同じビット数でなければならない。
// AとBはお互いにレイアウト互換
struct A
{
int x:8 ;
} ;
struct B
{
int x:8 ;
} ;
標準レイアウトunionが、お互いにレイアウト互換な複数の標準レイアウトstructを持つとき、先頭から共通のメンバーについては、一方を変更して、他方で使うこともできる。
struct A
{
int x ;
int y ;
float z ;
} ;
struct B
{
int x ;
int y ;
double z ;
} ;
union U
{
A a ;
B b ;
} ;
int main()
{
U u ;
u.a.x = 1 ;
u.a.y = 2 ;
// 以下の2式はtrueとなることが保証されている
u.b.x == 1 ;
u.b.y == 2 ;
// 3番目のメンバーは、レイアウト互換ではない
}
クラスの定義内で宣言される関数を、クラスのメンバー関数(member function)という。メンバー関数はstatic指定子と共に宣言することができる。その場合、関数はstaticメンバー関数となる。staticメンバー関数ではないメンバー関数のことを、非staticメンバー関数という。ただし、friend指定子と共に宣言された関数は、メンバー関数ではない。
struct S
{
void non_static_member_function() ; // 非staticメンバー関数
static void static_member_function() ; // staticメンバー関数
friend void friend_function() ; // これはメンバー関数ではない
} ;
メンバー関数は、クラス定義の内側でも外側でも定義することができる。クラス定義の内側で定義されたメンバー関数を、inlineメンバー関数という。これは、暗黙的にinline関数となる。クラス定義の外側でメンバー関数を定義する場合、クラスと同じ名前空間スコープ内で定義しなければならない。
struct S
{
// inlineメンバー関数
void inline_member_function()
{ /*定義*/ }
void member_function() ; // メンバー関数の宣言
} ;
// クラスSと同じ名前空間スコープ
void S::member_function()
{ /*定義*/ }
クラス定義の外側でinlineメンバー関数の定義をすることもできる。それには、関数宣言か関数定義で、inline指定子を使えばよい。
struct S
{
inline void f() ;
void g() ;
} ;
void S::f(){ }
inline void S::g() { }
クラス定義の外側でメンバー関数を定義するためには、メンバー関数の名前を、::演算子によって、クラス名で修飾しなければならない。
struct S
{
void member() ;
} ;
void S::member(){ }
ローカルクラスでは、メンバー関数をクラス定義の外側で宣言する方法はない。
非staticメンバー関数は、そのメンバー関数が属するクラスや、そのクラスから派生されたクラスのオブジェクトに対して、クラスメンバーアクセス演算子を使うことで、呼び出すことができる。同じクラスや、そのクラスから派生されたクラスのメンバー関数からは、他のメンバー関数を、通常の関数の呼び出しと同じように呼ぶことができる。
struct Object
{
void f(){ }
void g()
{
// 関数呼び出し
f() ;
}
} ;
int main()
{
Object object ;
// クラスのメンバーアクセス演算子と、関数呼び出し
object.f() ;
}
非staticメンバー関数は、CV修飾子と共に宣言することができる。
struct S
{
void none() ;
void c() const ; // constメンバー関数
void v() volatile ; // volatileメンバー関数
void cv() const volatile ; // const volatileメンバー関数
} ;
このCV修飾子は、thisポインターの型を変える。また、メンバー関数の型も、CV修飾子に影響される。
非staticメンバー関数は、リファレンス修飾子と共に宣言することができる。リファレンス修飾子は、オーバーロード解決の際の、暗黙の仮引数の型に影響を与える。詳しくは、候補関数と実引数リストを参照。
非staticメンバー関数は、virtualやピュアvirtualの文法で宣言することができる。詳しくは、virtual関数や、抽象クラスを参照。
非staticメンバー関数内では、thisというキーワードが、クラスのオブジェクトへのprvalueのポインターを表す。このクラスのオブジェクトは、メンバー関数を呼び出した際のクラスのオブジェクトである。
struct Object
{
void f()
{
this ;
}
} ;
int main()
{
Object a, b, c ;
a.f() ; // thisは&a
b.f() ; // thisは&b
c.f() ; // thisは&c
}
class Xのメンバー関数におけるthisの型は、X *である。もし、constメンバー関数の場合、X const *となり、volatileメンバー関数の場合、X volatile *となり、const volatileメンバー関数の場合は、X const volatile *となる。
class X
{
// thisの型はX *
void f() { this ; }
// thisの型はX const *
void f() const { this ; }
// thisの型はX volatile *
void f() volatile { this ; }
// thisの型はX const volatile *
void f() const volatile { this ; }
} ;
constメンバー関数では、メンバー関数に渡されるクラスのオブジェクトがconstであるため、thisの型も、constなクラスへのポインター型となり、非staticデータメンバーを変更することができない。
class X
{
int value ;
void f() const
{
this->value = 0 ; // エラー
}
} ;
これは、thisの型を考えてみると分かりやすい。
class X
{
int value ;
void f() const
{
X const * ptr = this ;
ptr->value = 0 ; // エラー
}
} ;
thisの型が、constなクラスに対するポインターなので、データメンバーを変更することはできない。
同様にして、volatileの場合も、非staticデータメンバーはvolatile指定されたものとみなされる。volatileの具体的な機能ついては、実装に依存する。
CV修飾されたメンバー関数は、同等か、より少なくCV修飾されたクラスのオブジェクトに対して呼び出すことができる。
struct X
{
X() { }
void f() const { } ;
} ;
int main()
{
X none ;
none.f() ; // OK、より少なくCV修飾されている
X const c ;
c.f() ; // OK、同じCV修飾
X const volatile cv ;
cv.f() ; // エラー、CV修飾子を取り除くことはできない
}
これは、非staticメンバー関数から、他の非staticメンバー関数を、クラスメンバーアクセスなしで呼び出す際にも当てはまる。その場合、クラスのオブジェクトとは、thisである。
struct X
{
void c() const // constメンバー関数
{
// thisの型はX const *
nc() ; // エラー、CV修飾子を取り除くことはできない
} ;
void nc() // 非constメンバー関数
{
// thisの型はX *
c() ; // OK、CV修飾子を付け加えることはできる
}
} ;
コンストラクターとデストラクターは、CV修飾子と共に宣言することができない。ただし、これらの特別なメンバー関数は、constなクラスのオブジェクトの生成、破棄の際にも、呼び出される。
クラスのデータメンバーやメンバー関数は、クラス定義内で、static指定子と共に宣言することが出来る。そのように宣言されたメンバーを、クラスのstaticメンバーという。
struct S
{
static int data_member ;
static void member_function() ;
} ;
int S::data_member ;
クラスXのstaticメンバーsは、::演算子を用いて、X::sのように参照することで、使うことができる。staticメンバーは、クラスのオブジェクトがなくても参照できるので、クラスのメンバーアクセス演算子を使う必要はない。しかし、クラスのメンバーアクセス演算子を使っても参照できる。
struct X
{
static int s ;
} ;
int X::s ;
int main()
{
// ::演算子による参照
X::s ;
// クラスのメンバーアクセス演算子でも参照することはできる
X object ;
object.s ;
}
クラスのメンバー関数内では、非修飾名を使った場合、クラスのstaticメンバー、enum名、ネストされた型が名前探索される。
struct X
{
void f()
{
s ; // X::s
value ; // X::value
type obj ; // X::type
}
static int s ;
enum { value } ;
typedef int type ;
} ;
int X::s ;
staticメンバーにも、通常通りのアクセス指定が適用される。
staticメンバー関数は、thisポインターを持たない。virtual指定子を使えない。CV修飾子を使えない。名前と仮引数の同じ、staticメンバー関数と非staticメンバー関数は、両立できない。
struct S
{
// エラー、同じ名前と仮引数のstatic関数と非static関数が存在する
void same() ;
static void same() ;
} ;
その他は、通常のメンバー関数と同じである。
staticデータメンバーは、クラスのサブオブジェクトには含まれない。staticデータメンバーは、クラスのスコープでアクセス可能なstatic変数だと考えてもよい。クラスのすべてのオブジェクトは、ひとつのstaticデータメンバーのオブジェクトを共有する。ただし、static thread_localなデータメンバーのオブジェクトは、スレッドにつきひとつ存在する。
struct S
{
static int member ;
} ;
int S::member ;
int main()
{
S a, b, c, d, e ;
// すべて同じオブジェクトを参照する
a.member ; b.member ; c.member ; e.member ;
}
クラス定義内のstaticデータメンバー宣言は、定義ではない。staticデータメンバーの定義は、クラスの定義を含む名前空間スコープの中に書かなければならない。その際、名前に::演算子を用いて、クラス名を指定する必要がある。staticデータメンバーの定義には、初期化子を使うことができる。初期化子は必須ではない。
struct S
{
static int member ;
} ;
// クラス名 :: メンバー名
int S::member = 0 ;
staticデータメンバーが、constなリテラル型の場合、クラス定義内の宣言に、初期化子を書くことができる。この場合、初期化子は定数式でなければならない。staticデータメンバー自体も、定数式になる。初期化子を書かない場合は、通常通り、クラス定義の外、同じ名前空間スコープ内で、定義を書かなければならない。この場合は、定数式にはならない。
struct S
{
static const int constant_expression = 123 ; // 定数式
static const int non_constant_expression ; // 宣言、定数式ではない
} ;
const int S::non_constant_expression = 123 ; // 定義
リテラル型のstaticデータメンバーは、constexpr指定子をつけて宣言することもできる。この場合、初期化子を書かなければならない。初期化子は、定数式でなければならない。このように定義されたstaticデータメンバーは、定数式になる。
struct S
{
static constexpr int constant_expression = 123 ; // 定数式
} ;
名前空間スコープのクラスのstaticデータメンバーは、外部リンケージを持つ。ローカルクラスのstaticデータメンバーは、リンケージを持たない。
staticデータメンバーは、非ローカル変数と同じように、初期化、破棄される。詳しくは、非ローカル変数の初期化、終了を参照。
staticデータメンバーには、mutable指定子は使えない。
unionというクラスは、クラスキーにunionキーワードを用いて宣言する。unionでは、非staticデータメンバーは、どれかひとつのみが有効である。これは、unionのオブジェクト内では、非staticデータメンバーのストレージは、共有されているからである。unionのサイズは、非staticデータメンバーのうち、最も大きな型を格納するのに十分なサイズとなる。
union U
{
int i ;
short s ;
double d ;
} ;
int main()
{
U u ;
u.i = 0 ; // U::iが有効
u.s = 0 ; // U::sが有効、U::iは有効ではなくなる
}
この例では、unionのサイズは、int, short, doubleのうちの、最もサイズが大きな型を格納するのに十分なだけのサイズである。データメンバーであるi, s, dは、同じストレージを共有している。
unionと、標準レイアウトクラスについては、クラスを参照。
unionは、通常のクラスに比べて、いくらか制限を受ける。unionはvirtual関数を持つことができない。unionは、基本クラスを持つことができない。unionは基本クラスになれない。unionは、リファレンス型の非staticデータメンバーを持つことができない。unionの非staticデータメンバーのうち、初期化子を持てるのは、ひとつだけである。
// エラーの例
// エラー、virtual関数を持てない
union U1 { virtual void f() { } } ;
// エラー、基本クラスを持てない
struct Base { } ;
union U : Base { } ;
// エラー、基本クラスになれない
union Union_base { } ;
union Derived : Union_base { } ;
// エラー、リファレンス型の非staticデータメンバーを持てない
union U2 { int & ref ; } ;
// エラー、非staticデータメンバーで、初期化子を持てるのは、ひとつだけ
union U3
{
int x = 0 ;
int y = 0 ; // エラー、複数の初期化子、どちらか一つならエラーではない
} ;
その他は、通常のクラスと変わることがない。unionはメンバー関数を持てる。メンバー関数には、コンストラクターやデストラクター、演算子のオーバーロードも含まれる。アクセス指定も使える。staticデータメンバーならば、リファレンス型でも構わない。ネストされた型も使える。
unionの非staticデータメンバーが、非trivialなコンストラクター、コピーコンストラクター、ムーブコンストラクター、コピー代入演算子、ムーブ代入演算子、デストラクターを持っている場合、unionの対応するメンバーが、暗黙的にdeleteされる。そのため、これらのメンバーを使う場合には、union側で、ユーザー定義しなければならない。
union U
{
std::string str ;
std::vector<int> vec ;
} ;
int main()
{
U u ; // エラー、コンストラクターとデストラクターがdelete定義されている。
}
この例では、strやvecは、非trivialなコンストラクターやデストラクターなどを持っているので、union側でも、それらを定義しなければならない。また、この例の場合、unionのコンストラクターやデストラクターは何もしないので、このunionを実際に使う場合には、placement newや、明示的なデストラクター呼び出しが必要になる。
union U
{
std::string str ;
std::vector<int> vec ;
U() { }
~U() { }
} ;
int main()
{
U u ;
new ( &u.str ) std::string( "hello" ) ;
u.str.~basic_string() ;
new ( &u.vec ) std::vector<int> { 1, 2, 3 } ;
u.vec.~vector() ;
}
もちろん、unionのコンストラクターやデストラクターで、どれかのデータメンバーの初期化、破棄をすることは可能である。しかし、どのデータメンバーが有効なのかということを、union内で把握するのは難しい。
以下のような形式のunionの宣言を、無名union(anonymous union)という。
union { メンバー指定opt } ;
無名unionは、無名の型のunionの、無名のオブジェクトを生成する。無名unionのメンバー指定は、非staticデータメンバーだけでなければならない。無名unionのメンバーの名前は、宣言されているスコープの他の名前と衝突してはならない。無名unionでは、staticデータメンバーやメンバー関数、ネストされた型などは使えない。また、privateやprotectedアクセス指定も使えない。
int main()
{
union { int i ; short s ; } ;
// iかsのどちらかひとつだけが有効
i = 0 ;
s = 0 ;
}
これは、以下のようなコードと同じであると考えることもできる。
int main()
{
union Anonymous { int i ; short s ; } unnamed ;
unnamed.i = 0 ;
unnamed.s = 0 ;
}
名前空間スコープで宣言される無名unionには、必ずstatic指定子をつけなければならない。
// グローバル名前空間スコープ
static union { int x ; int y ; } ;
名前空間スコープで宣言される無名unionは、staticストレージの有効期間と、内部リンケージを持つ。
ブロックスコープで宣言される無名unionは、ブロックスコープ内で許されているすべてのストレージ上に構築できる。
int main()
{
// 自動ストレージ
union { int a } ;
// staticストレージ
static union { int b } ;
// thread_localストレージ
thread_local union { int c } ;
}
クラススコープで宣言される無名unionには、ストレージ指定子を付けることはできない。
struct S
{
union
{
int x ;
} ;
} ;
オブジェクトやポインターを宣言している、クラス名の省略されたunionは、無名unionではない。
// クラス名の省略されたunion
// 無名unionではない
union { int x ; } obj, * ptr ;
union、もしくは無名unionを直接のメンバーに持つクラスを、unionのようなクラス(union-like class)という。unionのようなクラスには、共用メンバー(variant member)という概念が存在する。unionの共用メンバーは、unionの非staticデータメンバーである。無名unionを直接のメンバーに持つクラスの場合、無名unionの非staticデータメンバーである。
// xとyは共用メンバー
union U { int x ; int y ; }
// xとyは共用メンバー
struct S
{
union { int x ; int y ; } ;
} ;
ビットフィールドは、以下のようなメンバー宣言子の文法で宣言できる。
識別子opt : 定数式
定数式は、0よりも大きい整数でなければならない。
struct S
{
int // 型指定子
x : 8 ; // ビットフィールドの宣言子
} ;
ビットフィールドの定数式は、データメンバーのサイズをビット数で指定する。ビットフィールドに関しては、ほとんどの挙動が実装依存である。特に、ビットフィールドがクラスオブジェクトのストレージ上でどのように表現されるのかということや、アライメントなどは、すべて実装依存である。また、実装は、ビットフィールドのメンバー同士を詰めて表現することが許されている。
struct S
{
char x : 1 ;
char y : 1 ;
} ;
ここで、sizeof(S)は、2以上になるとは限らない。例えば、sizeof(S)が1を返す実装もあり得る。
ビットフィールドの定数式は、オブジェクトのビット数を上回ることができる。その場合、上回ったビット数は、パディングとして確保されるが、オブジェクトの内部表現として使われることはない。
struct S
{
int x : 1000 ;
} ;
ここで、sizeof(S)は、少なくとも1000ビット以上になる値を返す(規格では、1バイトあたりのビット数は定められていない)。ただし、S::xは、本来のint型以上の範囲の値を保持することはできない。int型のオブジェクトのビット数を上回った分は、単にパディングとして確保されているに過ぎない。
ビットフィールドの宣言子で、識別子を省略した場合、無名ビットフィールドとなる。無名ビットフィールドはクラスのメンバーではなく、初期化もされない。ただし、実装依存の方法で、クラスのオブジェクト内に存在する。一般的な実装では、無名ビットフィールドは、オブジェクトのレイアウトを調整するためのパディングとして用いられる。
struct S
{
int x : 4 ;
char : 3 ; // 無名ビットフィールド
int y : 1 ;
} ;
あるコンパイラーでは、このような無名ビットフィールドにより、S::xとS::yの間に、3ビット分のパディングを挿入することができる。ただし、すでに述べたように、ビットフィールドの内部表現とアライメントは実装依存なので、これはすべてのコンパイラーに当てはまるわけではない。使用しているコンパイラーが、ビットフィールドをどのように実装しているかは、コンパイラー独自のマニュアルを参照すべきである。
無名ビットフィールドでは、特別に、定数式に0を指定することができる。
struct S
{
int x : 4 ;
char : 0 ; // 無名ビットフィールド
int y : 4 ;
} ;
これは、無名ビットフィールドの次のビットフィールドのアライメントを、アロケーション単位の境界に配置させるための指定である。上記の構造体は、ある環境では、S::xとS::yが同一のアロケーション単位に配置されるかもしれないが、無名ビットフィールドを使うことで、X::yを別のアロケーション単位に配置できる。
ビットフィールドはstaticメンバーにはできない。ビットフィールドの型は、整数型かenum型でなければならない。符号が指定されていない整数型のビットフィールドの符号は実装依存である。
struct S
{
static int error : 8 ; // エラー、ビットフィールドはstaticメンバーにはできない
int impl : 8 ; // 符号は実装依存
signed s : 8 ; // 符号はsigned
unsigned u : 8 ; // 符号はunsigned
} ;
bool型のビットフィールドは、ビット数に関わらず、bool型の値を表現できる。
struct S
{
bool a : 1 ;
bool b : 2 ;
bool c : 3 ;
} ;
int main()
{
S s ;
s.a = true ;
s.b = false ;
s.c = true ;
}
ビットフィールドのアドレスを得ることはできない。つまり、&演算子をビットフィールドに適用することはできない。
struct S
{
int x : 8 ;
} ;
int main()
{
S s ;
&s.x ; // エラー
}
リファレンスは、ビットフィールドを参照することはできない。ただし、constなlvalueリファレンスの初期化子が、lvalueのビットフィールドの場合は、一時オブジェクトが生成され、そのオブジェクトを参照する。
struct S
{
int x : 8 ;
} ;
int main()
{
S s ;
// エラー
int & ref = s.x ;
// OK
// ただし、crefが参照するのは、生成された一時オブジェクトである
// s.xではない
int const & cref = s.x ;
}
クラスは、他のクラスの内側で宣言することができる。これを、ネストされたクラス(nested class)という。
class Outer
{
class Inner { } ; // ネストされたクラス
} ;
int main()
{
Outer::Inner object ;
}
ネストされたクラスのスコープは、外側のクラスのスコープに従う。これは、名前探索の際も、外側のクラスのスコープが影響するということである。
int x ; // グローバル変数
struct Outer
{
int x ; // Outer::x
struct Inner
{
void f()
{
sizeof(x) ; // OK、sizeofのオペランドは未評価式。Outer::xのサイズを返す
x = 0 ; // エラー、Outer::xはOuterの非staticメンバー
::x = 0 ; // OK、グローバル変数
}
} ;
} ;
関数Inner::fの中で、xという名前を使うと、Outer::xが見つかる。これは、クラスInnerが、クラスOuterのスコープ内にあるためである。しかし、非staticデータメンバーであるOuter::xを使うためには、Outerのオブジェクトが必要なので、ここではエラーとなる。sizeofのオペランドは未評価式なので、問題はない。ただし、ここでのxは、Outer::xである。グローバル変数のxではない。
ネストされたクラスのメンバー関数やstaticデータメンバーは、通常のクラス通り、クラス定義の外側、同じ名前空間内で定義することができる。
// グローバル名前空間
struct Outer
{
struct Inner
{
static int x ;
void f() ;
} ;
} ;
// 同じ名前空間内
int Outer::Inner::x = 0 ;
void Outer::Inner::f() { }
また通常通り、クラスの宣言だけをして、定義を後で書くこともできる。
struct Outer
{
class Inner ; // 宣言
} ;
class Outer::Inner { } ; // 定義
関数定義の中で、クラスを定義することができる。これをローカルクラス(local class)という。
int main()
{ // 関数定義
class Local { } ; // ローカルクラス
Local object ; // ローカルクラスのオブジェクト
}
ローカルクラスのスコープは、クラス定義の外側のスコープである。また、名前探索は、ローカルクラスが定義されている関数と同じとなる。
ローカルクラスは、定義されている関数内の自動変数を使うことは出来ない。typedef名やstatic変数などは使える。
int main()
{
int x ; // ローカル変数
typedef int type ;
static int y ;
class Local
{
void f()
{
x = 0 ; // エラー
// typedef名やstatic変数などは使える
type val ; // OK
y = 0 ; // OK
// OK、sizeofのオペランドは未評価式
sizeof(x) ;
}
} ;
}
ローカルクラスは、通常のクラスより制限が多い。ローカルクラスをテンプレート宣言することはできない。メンバーテンプレートを持つこともできない。ローカルクラスのメンバー関数は、クラス定義内で定義されなければならない。ローカルクラスの外側でメンバー関数を定義する方法はない。staticデータメンバーを持つことはできない。
int main()
{
// エラー、ローカルクラスはテンプレート宣言できない
template < typename T >
class Local
{
// エラー、ローカルクラスはメンバーテンプレートを持てない。
template < typename U > void f() { }
// OK、ただし、ローカルクラスの外側でメンバー関数を定義する方法はない
void f() ;
// エラー、ローカルクラスはstaticデータメンバーを持つことはできない。
static int x ;
} ;
}
クラス内の型名を、ネストされた型名(nested type name)という。ネストされた型名を、クラスの外側で使うには、クラス名による修飾が必要である。
struct X
{
typedef int I ;
class Inner { } ;
I member ;
} ;
X::I object ;
クラスには、基本クラス指定によって基本クラスを指定することができる。基本クラス指定は、以下のような文法である。
基本句:
: 基本指定子リスト
基本指定子リスト:
基本指定子 ...opt
基本指定子リスト, 基本指定子 ...opt
基本指定子:
アトリビュート指定子opt 基本型指定子
アトリビュート指定子opt virtual アクセス指定子opt 基本型指定子
アトリビュート指定子opt アクセス指定子 virtualopt 基本型指定子
基本型指定子:
クラスもしくはdecltype
アクセス指定子:
private
protected
public
基本クラス指定子に指定されたクラスのことを、基本クラス(base class)という。また、基本クラスを指定したクラスを、基本クラスに対する、派生クラス(derived class)という。クラスの基本クラス指定に指定されているクラスを、クラスの直接の基本クラス(direct base class)という。基本クラス指定には指定されていないものの、直接の基本クラスを通じて基本クラスとなっているクラスを、クラスの間接の基本クラス(indirect base class)という。単に基本クラスという場合、直接の基本クラスと間接の基本クラスの両方を意味する。
他のプログラミング言語の中には、基本クラスのことをスーパークラスと呼び、派生クラスのことをサブクラスと名付けている言語もある。C++では、そのような名称は用いない。これは、スーパーとサブでは意味が分かりにくいと、他ならぬBjarne Stroustrup自身が考えたためである。そのため、C++では、スーパーのかわりに基本(base)、サブのかわりに派生(derived)という言葉を用いることになった。
基本クラスは、基本指定子に記述する。これは、
struct Base { } ;
struct Derived1 : Base { } ;
struct Derived2 : Derived1 { } ;
ここでは、Derived1の基本クラスはBaseである。Derived2の基本クラスはDerived1とBaseである。Derived2の直接の基本クラスはDerived1、間接の基本クラスはBaseである。
派生(derived)と継承(inherited)という言葉には、規格上、明確な違いがある。
派生という言葉は、派生クラスと基本クラスの関係を記述するために用いられる。あるクラスが基本クラスを持つ場合、「あるクラスは、基本クラスから、派生される(A class is derived from its base class)」という。「DerivedクラスはBaseクラスから派生されている(The Derived class is derived from the Base class.)」といえば、以下のようなコードを意味する。
struct Base { } ;
struct Derived : Base { } ;
この場合、DerivedクラスはBaseクラスから派生されている、という。クラスの派生関係に、継承という言葉を使うのは誤りである。
ただし、継承されている基本クラス(inherited base class)という言い方をすることはある。これは、対象が基本クラスで、これに対して派生という言葉を用いると、派生クラスという意味になってしまうからだ。
継承という言葉は、クラスのメンバーに対して用いられる。「基本クラスのメンバーは、派生クラスに継承される(The Base class's member is inherited by the derived class.)」という。例えば、「Baseクラスのメンバー関数fは、Derivedクラスに、継承されている(The Base class's member function f is inherited by the Derived class)」といえば、以下のようなコードを意味する。
struct Base { void f() ; } ;
// DerivedはBase::fを継承
struct Derived : Base { } ;
クラスのメンバーに対して、派生という言葉は使うのは誤りである。
基本クラス指定子に...が使われた場合、パック展開とみなされる。
template < typename ... Types >
struct X : Types ... { } ;
アクセス指定については、メンバーのアクセス指定を参照。
基本クラスは、複数指定することができる。これを、複数の基本クラスという。複数の基本クラスを指定することを、俗に、多重継承(Multiple Inheritance)ということがあるが、これは、C++の規格上、正しい用語ではない。継承は、基本クラスのメンバーを派生クラスも受け継ぐことを意味する用語であって、クラスの派生関係を表すのに使う言葉ではないからだ。
ただし、歴史的に言えば、Multiple Inheritanceという言葉を最初に使ったのは、他ならぬBjarne Stroustrupご本人である。当時、Stroustrup氏が複数の基本クラスの設計をしていた時に使った言葉が、多重継承であった。ちなみに、多重継承が初めて使われたコードは、Jerry Schwarzによって書かれたiostreamである。
複数の基本クラスは、コンマで区切ることによって指定する。
struct A { } ; struct B { } ; struct C { } ;
struct D
: A, B, C
{ } ;
この例では、Dは、A、B、Cという3個の基本クラスを持っている。
同じクラスを複数、直接の基本クラスとして指定することは出来ない。間接の基本クラスとしては指定できる。
struct Base { } ;
struct Derived
: Base, Base // エラー、直接の基本クラス
{ } ;
struct Derived1 : Base { } ;
struct Derived2 : Base { } ;
struct Derived3
: Derived1, Derived2 // OK、間接の基本クラス
{ } ;
この場合、Derived3は、Baseクラスのサブオブジェクトを、2個持つことになる。
基本クラスに、virtualが指定されていない場合、非virtual基本クラス(non-virtual base class)となる。非virtual基本クラスには、それぞれ独立したサブオブジェクトが割り当てられる。
同じクラスが複数、非virtual基本クラスとして存在することは、基本クラスのメンバーの名前に対するオブジェクトが容易に曖昧になる。このとき、派生クラスから基本クラスのメンバーを使うには、名前を正しく修飾しなければならない。
struct Base { int member ; } ;
struct Derived1 : Base { } ;
struct Derived2 : Base { } ;
// Derived3には、2個のBaseサブオブジェクトが存在する
struct Derived3 : Derived1, Derived2
{
void f()
{
member ; // エラー、曖昧
Base::member ; // エラー、曖昧
Derived1::member ; // OK
Derived2::member ; // OK
}
} ;
int main()
{
Derived3 x ;
x.member ; // エラー、曖昧
x.Derived1::member ; // OK
x.Derived2::member ; // OK
}
ただし、staticメンバーの名前は、曖昧にならない。これは、staticメンバーの利用には、クラスのオブジェクトは必要ないからである。
struct Base
{
static void static_member() { }
static int static_data_member ;
} ;
int Base::static_data_member = 0 ;
struct Derived1 : Base { } ;
struct Derived2 : Base { } ;
struct Derived3 : Derived1, Derived2
{
void f()
{
static_member() ; // OK
static_data_member ; // OK
}
} ;
直接、間接の両方の基本クラスに、同じクラスを持つことは可能である。ただし、そのような派生クラスは、基本クラスの非staticメンバーを使うことができない。なぜなら、基本クラスの名前自体の曖昧性を解決する方法がないからだ。
struct Base
{
int member() ; // 非staticメンバー
static void static_member() { } // staticメンバー
} ;
struct Derived1 : Base { } ;
// Baseという名前自体が曖昧になる
struct Derived2 : Base, Derived1
{
void f()
{
// Baseの非staticメンバーを使う方法はない
static_member() ; // OK、staticメンバーは使える
}
} ;
このため、直接、間接の両方で同じクラスを基本クラスに持つ派生クラスの利用は、かなり制限される。
基本クラスに、virtualが指定されている場合、virtual基本クラス(virtual base class)という。virtual基本クラスには、ひとつしかオブジェクトが割り当てられない。virtual基本クラスのオブジェクトは、派生クラスで共有される。
struct L { } ;
struct A : virtual L { } ;
struct B : virtual L { } ;
struct C : A, B { } ;
この例で、Cクラスには、Lのサブオブジェクトは1個存在する。これは、A、Bで共有される。
virtual基本クラスでは、サブオブジェクトが共有されているため、virtual基本クラスのメンバーは、曖昧にならない。
struct Base { int member ; } ;
struct Derived1 : virtual Base { } ;
struct Derived2 : virtual Base { } ;
struct Derived3 : Derived1, Derived2
{
void f()
{
member ; // OK
}
} ;
非virtual基本クラスとvirtual基本クラスは、両方持つことができる。
struct B { } ;
struct X : virtual B { } ;
struct Y : virtual B { } ;
struct Z : B { } ;
struct A : X, Y, Z { } ;
この例では、Aクラスには、Bのサブオブジェクトは、2個存在する。X、Yで共有されるサブオブジェクトと、Zのサブオブジェクトである。
メンバーの名前探索は、すこし難しい。派生クラスのメンバー名は、基本クラスのメンバー名を隠すということだ。あるメンバー名を名前探索する際に、派生クラスで名前が見つかった場合、その時点で名前探索は終了する。基本クラスのメンバーを探すことはない。
struct Base
{
void f( int ) { }
} ;
struct Derived : Base
{
void f( double ) { }
} ;
int main()
{
Derived object;
object.f( 0 ) ; // Derived::f( double )が呼ばれる
}
ここで、Derivedクラスには、二つのfという名前のメンバーが存在する。Derived::fとBase::fである。もし、名前探索によって両方の名前が発見された場合、オーバーロード解決によって、Base::f(int)が選ばれるはずである。しかし、実際には、Derived::f(double)が選ばれる。これは、Derivedクラスに、fという名前のメンバーが存在するので、その時点で名前探索が終了するからである。Baseのメンバー名は発見されない。名前が発見されない以上、オーバーロード解決によって選ばれることもない。
これは、名前探索に対するルールなので、型は関係がない。
// fという名前のint型のデータメンバー
struct Base { int f ; } ;
// fという名前のvoid (void)型のメンバー関数
struct Derived : Base { void f( ) { } } ;
int main()
{
Derived object;
object.f = 0 ; // エラー、メンバー関数Derived::fに0を代入することはできない
object.Base::f = 0 ; // OK、明示的な修飾
}
したがって、基本クラスと同じ名前のメンバーを派生クラスで使う際には、注意が必要である。
名前探索という仕組みを考えずに、この挙動を考えた場合、これは、派生クラスのメンバー名が、基本クラスのメンバー名を、隠していると考えることもできる。もし、基本クラスのメンバー名を隠したくない場合、using宣言を使うことができる。using宣言を使うと、基本クラスのメンバー名を、派生クラスのスコープに導入することができる。
struct Base
{
void f( int ) { }
} ;
struct Derived : Base
{
using Base::f ; // using宣言
void f( double ) { }
} ;
int main()
{
Derived object;
object.f( 0 ) ; // Base::f( int )が呼ばれる
}
名前探索で、派生クラスのメンバーが見つからない場合は、直接の基本クラスのメンバーから、名前が探される。
struct Base { int member ; } ;
struct Derived : Base
{
void f()
{
member ; // Base::member
}
} ;
メンバー名を探す基本クラスは、直接の基本クラスだけである。間接の基本クラスのメンバーは、直接の基本クラスを通じて、探される。
struct A { int member ; } ;
struct B : A { } ;
struct C : B
{
void f()
{
member ; // A::member
}
} ;
この例では、C::fでmemberという名前のメンバーを使っている。Cクラスにはmemberという名前のメンバーが見つからないので、名前探索はBクラスに移る。クラスは、基本クラスのメンバー名を継承している。そのため、Bクラスの基本クラスのAクラスのメンバー名は、Bクラスのスコープからも発見することができる。
直接の基本クラスが複数ある場合、それぞれの直接の基本クラスから、名前が探される。この際、複数のクラスから同じ名前が発見され、名前の意味が違う場合、名前探索は無効となる。
struct Base1 { void member( int ) { } } ;
struct Base2 { void member( double ) { } } ;
struct Derived : Base1, Base2 // 複数の直接の基本クラス
{
void f()
{
member( 0 ) ; // エラー、名前探索が無効
Base1::member( 0 ) ; // OK
}
} ;
これは、memberという名前に対し、複数の直接の基本クラスで、複数の同じ名前が見つかり、しかも意味が違っているので、名前検索が無効となる。その結果、memberという名前が見つからず、エラーとなる。
もし、この例で、Derivedから、明示的な修飾をせずに、両方の基本クラスのメンバー関数を呼び出したい場合、using宣言が使える。
struct Base1 { void member( int ) { } } ;
struct Base2 { void member( double ) { } } ;
struct Derived : Base1, Base2
{
// 基本クラスのメンバー名をDerivedスコープで宣言する
using Base1::member ;
using Base2::member ;
void f()
{
member( 0 ) ; // OK、オーバーロード解決により、Base1::member(int)が呼ばれる
}
} ;
この例は、複数の直接の基本クラスがある場合の制限である。複数の間接の基本クラスでは、名前探索が失敗することはない。ただし、名前探索の結果として、複数の名前が発見され、曖昧になることはある。
本書のサンプルコードは、解説する文法のための最小限のコードであり、virtual関数を持つクラスがvirtualデストラクターを持たないことがある。これは現実ではほとんどの場合、不適切である。
メンバー関数にvirtual指定子を指定すると、virtual関数となる。virtual関数を宣言しているクラス、あるいはvirtual関数を継承しているクラスは、ポリモーフィッククラス(polymorphic class)となる。
struct Base
{
virtual void f() { } // virtual関数
} ;
struct Derived : Base { } ;
BaseとDerivedは、ポリモーフィッククラスである。
クラスがポリモーフィックであるかどうかということは、dynamic_castやtypeidを使う際に、重要である。
基本クラスのvirtual関数は、派生クラスのメンバーに、同じ名前、同じ仮引数リスト、同じCV修飾子、同じリファレンス修飾子という条件を満たすメンバー関数があった場合、オーバーライドされる。この時、派生クラスのメンバー関数は、virtual指定子がなくても、自動的にvirtual関数になる。
struct A { virtual void f() {} } ;
struct B : A { } ; // オーバーライドしない
struct C : A
{
void f() { } // オーバーライド
} ;
struct D : C
{
void f(int) { } // オーバーライドしない
void f() const { } // オーバーライドしない
} ;
// リファレンス修飾子が違う例
struct Base { virtual void f() & { } } ;
struct Derived : Base { void f() && { } } ;
もちろん、virtualをつけてもよい。
struct Base { virtual f() { } } ;
struct Derived : Base { virtual f() { } } ; // オーバーライド
派生クラスで、最後にオーバーライドしたvirtual関数を、ファイナルオーバーライダー(final overrider)と呼ぶ。あるクラスのオブジェクトに対して、virtual関数を呼び出す際は、オブジェクトの実行時の型によって、最後にオーバーライドしたvirtual関数が呼び出される。これは、基本クラスのポインターやリファレンスを経由してオブジェクトを使った場合でも、同様である。通常のメンバー関数は、virtual関数とは違い、実行時の型チェックを行わない。オブジェクトを指しているリファレンスやポインターの型によって、決定される。
// virtual関数と非virtual関数の違いの例
struct A
{
virtual void virtual_function() { }
void function() { }
} ;
struct B : A
{
virtual void virtual_function() { }
void function() { }
} ;
struct C : B
{
virtual void virtual_function() { }
void function() { }
} ;
void call( A & ref )
{
ref.virtual_function() ;
ref.function() ;
}
int main()
{
A a ; B b ; C c ;
call( a ) ; // A::virtual_function, A::functionが呼び出される
call( b ) ; // B::virtual_function, A::functionが呼び出される
call( c ) ; // C::virtual_function, A::functionが呼び出される
}
Aは、virtual_functionとfunctionという名前のvirtual関数を持っており、Aから派生しているB、Bから派生しているCは、オーバーライドしている。call関数の仮引数refは、オブジェクトの型が、実際に何であるかは、実行時にしか分からない。virtual関数であるvirtual_functionは、オブジェクトの型に合わせて正しく呼び出されるが、virtual関数ではないfunctionは、Aのメンバーが呼び出される。
virt指定子(virt-specifier)は、finalかoverrideで、virtual関数の宣言子の後、pure指定子の前に記述できる。
// virt指定子の文法の例示のための記述
virtual f() final override = 0 ;
finalが指定されたvirtual関数を持つクラスから派生したクラスが、同virtual関数をオーバーライドした場合はエラーになる。
struct base
{
virtual void f() { }
} ;
struct derived
{
virtual void f() final { }
} ;
struct ok : derived
{
// OK
} ;
struct error : derived
{
// エラー、final指定されているderived::fをオーバーライド
virtual void f() { }
} ;
virtual関数にfinalを指定すると、それ以上のオーバーライドを禁止できる。
overrideが指定されたvirtual関数が、基本クラスのメンバー関数をオーバーライドしていない場合、エラーとなる。
struct base
{
virtual void virtual_function() { }
} ;
struct ok : base
{
// OK、ok::virtual_functionはbase::virtual_functionをオーバーライドしている
virtual void virtual_function() override { }
} ;
struct typo : base
{
// OK、typo::virtal_functionはbase::virtual_functionとは別のvirtual関数
virtual void virtal_function() { }
} ;
struct error : base
{
// エラー、error::virtal_functionはオーバーライドしていない
virtual void virtal_function() override { }
} ;
これにより、タイプミスによる些細な間違いをコンパイル時に検出できる。
オーバーライドであることに注意。以下のコードはエラーである。
struct base
{
void f() { } // 非virtual関数
} ;
struct error : base
{
// エラー、オーバーライドしていない
virtual void f() override { }
} ;
finalとoverrideを両方指定することもできる。
virtual関数をオーバーライドする関数は、戻り値の型が同じでなくても構わない。ただし、何でもいいというわけではない。戻り値の型は、まったく同じ型か、相互変換可能(covariant)でなければならない。covariantは、以下のような条件をお互いに満たした型のことである。
今、関数D::fが、関数B::fをオーバーライドしているとする。
// D::f、B::fの例
struct B { virtual 戻り値の型 f() ; } ;
struct D : B { virtual 戻り値の型 f() ; } ;
その場合、戻り値の型は、以下の条件を満たさなければならない。
お互いにクラスへのポインター、もしくは、お互いにクラスへのlvalueリファレンス、もしくは、お互いにクラスへのrvalueリファレンスであること。
片方がポインターで片方がリファレンスの場合や、片方がlvalueリファレンスで片方がrvalueリファレンスの場合は、不適である。もちろん、ポインターでもリファレンスでもない型は不適である。また、クラスでもない型へのポインターやリファレンスも不適である。
// ポインター
struct B { virtual B * f() ; } ;
struct D : B { virtual D * f() ; } ;
// lvalueリファレンス
struct B { virtual B & f() ; } ;
struct D : B { virtual D & f() ; } ;
// rvalueリファレンス
struct B { virtual B && f() ; } ;
struct D : B { virtual D && f() ; } ;
B::fの戻り値の型のクラスは、D::fの戻り値の型のクラスと同じか、曖昧がなくアクセスできる基本クラスでなければならない。
オーバーライドしている関数が、基本クラスを戻り値に使っていたり、そもそもクラスの派生関係にない場合は、不適である。private派生していて、派生クラスからはアクセスできない場合や、基本クラスのサブオブジェクトが複数あって曖昧な場合はエラーとなる。
struct Base { } ; // 基本クラス
struct Derived : Base { } ; // 派生クラス
struct Other { } ; // BaseやDerivedとは派生関係にないクラス
// クラスが同じ
struct B { virtual Base & f() ; } ;
struct D : B { virtual Base & f() ; } ;
// B::fのクラスはD::fのクラスの基本クラス
struct B { virtual Base & f() ; } ;
struct D : B { virtual Derived & f() ; } ;
// エラー
struct B { virtual Derived & f() ; } ;
struct D : B { virtual Base & f() ; } ;
// エラー
struct B { virtual Base & f() ; } ;
struct D : B { virtual Other & f() ; } ;
両方のポインターは同じCV修飾子を持たなければならない。D::fの戻り値の型のクラスは、B::fの戻り値の型のクラスと同じCV修飾子を持つか、あるいは少ないCV修飾子を持たなければならない。
補足:ポインターに対するCV修飾子とは、T cv1 * cv2という型がある場合、cv2である。クラスに対するCV修飾子は、cv1である。
// int *に対するCV修飾子
int * const
// intに対するCV修飾子
const int *
int const *
// 両方のポインターは同じCV修飾子を持たなければならない例
// ポインターのCV修飾子はconst
struct B { virtual B * const f() ; } ;
// OK
struct D : B { virtual D * const f() ; } ;
// エラーのDクラスの例、ポインターのCV修飾子が一致していない
struct D : B { virtual D * f() ; } ;
struct D : B { virtual D * volatile const f() ; } ;
struct D : B { virtual D * const volatile f() ; } ;
// D::fの戻り値の型のクラスは、B::fの戻り値の型のクラスと同じCV修飾子を持つか、
// あるいは少ないCV修飾子を持たなければならない例
// B::fの戻り値の型のクラスのCV修飾子はconst
struct B { virtual B const & f() ; } ;
// 問題ないDクラスの例、CV修飾子が同じか少ない
struct D : B { virtual D const & f() ; } ;
struct D : B { virtual D & f() ; } ;
// エラーのDクラスの例、CV修飾子が多い
struct D : B { virtual D volatile & f() ; } ;
struct D : B { virtual D const volatile & f() ; } ;
明示的な修飾を用いた場合は、virtual関数呼び出しが阻害される。これは、オーバーライドしたvirtual関数から、オーバーライドされたvirtual関数を呼び出すのに使える。
struct Base { virtual void f() { } } ;
struct Derived : Base
{
virtual void f()
{
f() ; // Derived::fの呼び出し
Base::f() ; // 明示的なBase::fの呼び出し
}
} ;
virtual関数とdelete定義は併用できる。ただし、delete定義のvirtual関数を、非delete定義のvirtual関数でオーバーライドすることはできない。非delete定義のvirtual関数を、delete定義のvirtual関数でオーバーライドすることはできない。
// OK、delete定義のvirtual関数を、delete定義のvirtual関数でオーバーライドしている
struct Base { virtual void f() = delete ; } ;
struct Derived : Base { virtual void f() = delete ; } ;
// エラー、非delete定義ではないvirtual関数を、delete定義のvirtual関数でオーバーライドしている
struct Base { virtual void f() { } } ;
struct Derived : Base { virtual void f() = delete ; } ;
// エラー、delete定義のvirtual関数を、非delete定義のvirtual関数でオーバーライドしている
struct Base { virtual void f() = delete ; } ;
struct Derived : Base { virtual void f() { } } ;
ピュア指定子:
= 0
アブストラクトクラス(abstract class)は、抽象的な概念としてのクラスを実現する機能である。これは、例えば図形を表すクラスである、CircleやSquareなどといったクラスの基本クラスであるShapeや、動物を表すDogやCatなどといったクラスの基本クラスであるAnimalなど、異なるクラスに対する共通のインターフェースを提供する目的に使える。
struct Shape
{
// 図形描画用の関数
// Shapeクラスは抽象的な概念であり、具体的な描画方法を持たない
// 単に共通のインターフェースとして提供される
virtual void draw() = 0 ;
} ;
struct Circle : Shape
{
virtual void draw() { /* 円を描画 */ }
} ;
struct Square : Shape
{
virtual void draw() { /* 正方形を描画 */ }
} ;
void f( Shape * ptr )
{
ptr->draw() ; // 実行時の型に応じて図形を描画する
}
ここでは、Shapeクラスというのは、具体的に描画する方法を持たない。そもそも、Shapeクラス自体のオブジェクトを使うことは想定されていない。このように、そのクラス自体は抽象的な概念であり、実体を持たない場合、ピュアvirtual関数を使うことで、共通のインターフェースとすることができる。
他の言語では、この機能を明確にクラスから分離して、「インターフェース」という名前の機能にしているものもある。C++では、抽象クラスも、制限はあるものの、クラスの一種である。
少なくともひとつのピュアvirtual関数を持つクラスは、アブストラクトクラスとなる。ピュアvirtual関数は、virtual関数の宣言に、ピュア指定子を書くことで宣言できる。
ピュア指定子:
= 0
struct abstract_class
{
virtual void f() = 0 ;
} ;
ピュアvirtual関数は、呼ばれない限り、定義する必要はない。
struct Base
{
virtual void f() = 0 ;
virtual void g() = 0 ;
} ;
struct Derived
{
virtual void g() { }
} ;
void call_g( Base & base )
{
base.g() ;
}
int main()
{
Derived d ;
call_g( d ) ;
}
一つの関数宣言にピュア指定子と定義を両方書くことはできない。
struct X
{
// エラー
virtual void f() = 0 { } ;
} ;
ただし、複数の関数宣言を使えば、ひとつの関数にピュア指定子と定義を両方与えることができる。
struct X
{
// OK、ピュア指定子を与える関数宣言
virtual void f() = 0 ;
} ;
// OK、定義を与える関数宣言
void X::f() { }
この仕様は、オブジェクトの破棄の際に何らかの処理を行いたい抽象クラスに使うことができる。デストラクターを純粋仮想関数かつ定義付きの関数とすることができる。
class Base
{
int * ptr ;
public :
Base( int value )
: ptr( new int(value) )
{ }
virtual ~Base() = 0 ;
} ;
Base::~Base()
{
delete ptr ;
}
ただし、デストラクターの呼び出しは、通常のメンバー関数とは異なっているので、注意が必要である。以下のコードを考える。
struct Base
{
virtual void f() = 0 ;
virtual ~Base() = 0 ;
} ;
void Base::f() { }
Base::~Base
{
// Derivedはすでに破棄されている。
f() ; // エラー、Base::fのvirtual関数呼び出しの挙動は未定義
}
struct Derived : Base
{
virtual void f() { }
virtual ~Base() { }
} ;
int main()
{
Derived d ;
}
Baseのデストラクター呼び出しは問題がない。なぜならば、基本クラスのデストラクターは、あたかも明示的に直接呼び出されたかのように振る舞うからだ。virtual関数呼び出しではない。
ただし、デストラクターの中で未修飾名のfを呼び出すと、これはvirtual関数呼び出しになる。オブジェクトの構築中、破棄中にvirtual関数を呼び出した場合、オブジェクトの型が、あたかも最終的な派生クラスの型とみなされる。そのため、ここではBase::fがvirtual関数呼び出しされる。
Base::fは、定義が与えられてはいるものの、依然としてピュアvirtual関数であることに変わりはない。ピュアvirtual関数をvirtual関数呼び出しした場合の挙動は未定義である。そのため、上記のコードは、以下のように、明示的な修飾名で呼び出し、通常の関数呼び出しにしなければ、規格上、動作が保証されない。
Base::~Base
{
Base::f() ; // OK、修飾名は通常の関数呼び出し
}
この、=0という文法は、初期化子や代入式とは、何の関係もない。ただ、C++の文法上、メンバー関数の宣言の中の、=0というトークン列を、特別な意味を持つものとして扱っているだけである。ピュア指定子を記述する位置は、virt-specifierの後である。
struct Base { virtual void f() { } }
struct abstract_class : Base
{
virtual void f() override = 0 ; // virt-specifierの後
} ;
アブストラクトクラスは、他のクラスの基本クラスとして使うことしかできない。アブストラクトクラスのオブジェクトは、派生クラスのサブオブジェクトとしてのみ、存在することができる。
struct abstract_class
{
virtual void f() = 0 ;
} ;
struct Derived : abstract_class
{
void f() { }
} ;
アブストラクトクラスのオブジェクトを、直接作ることはできない。これには、変数や関数の仮引数、new式などが該当する。
struct abstract_class
{
virtual void f() = 0 ;
} ;
// エラー、abstract_classのオブジェクトは作れない
void f( abstract_class param )
{
abstract_class obj ; // エラー
new abstract_class ; // エラー
}
アブストラクトクラスへのポインターやリファレンスは使える。
struct abstract_class
{
virtual void f() = 0 ;
} ;
// OK、ポインターとリファレンスはよい
void f( abstract_class *, abstract_class & ) ;
ピュアvirtual関数を継承していて、ファイナルオーバーライダーがピュアvirtual関数である場合も、アブストラクトクラスとなる。これは例えば、アブストラクトクラスから派生されているクラスが、ピュアvirtual関数をオーバーライドしていなかった場合などが、該当する。
struct Base { virtual void f() = 0 ; } ;
struct Derived : Base { } ;
この場合、Derivedも、Baseと同じく、アブストラクトクラスになる。
派生クラスによって、ピュアvirtual関数ではないvirtual関数をオーバーライドして、ピュアvirtual関数にすることができる。その場合、派生クラスはアブストラクトクラスとなる。
struct Base { virtual void f() { } } ;
struct Derived : Base { virtual void f() = 0 ; } ;
int main()
{
Base b ; // OK
Derived d ; // エラー
}
この例では、Baseはアブストラクトクラスではない。Derivedはアブストラクトクラスである。
構築中、または破棄中のアブストラクトクラスのコンストラクターやデストラクターの中で、ピュアvirtual関数を呼び出した場合の挙動は、未定義である。
struct Base
{
virtual void f() = 0 ;
// この関数を、Baseのコンストラクターやデストラクターから呼ぶとエラー
void g()
{ f() ; }
// コンストラクター
Base() // エラー、未定義の挙動
{ f() ; }
// デストラクター
~Base() // エラー、未定義の挙動
{ f() ; }
} ;
struct Derived : Base
{
virtual void f() { }
// Derivedはアブストラクトクラスではないので、問題はない
Derived() { f() ; }
~Derived() { f() ; }
} ;
クラスのメンバーは、private、protected、publicのいずれかのアクセス指定を持つ。
privateが指定されたメンバーは、同じクラスのメンバーとfriendから使うことができる。
protectedが指定されたメンバーは、同じクラスと、そのクラスから派生されているクラスのメンバーとfriendから使うことができる。
publicでは、メンバーはどこからでも制限なく使える。
class Base
{
private :
int private_member ;
protected :
int protected_member ;
public :
int public_member ;
void f()
{
// 同じクラスのメンバー
private_member ; // OK
protected_member ; // OK
public_member ; // OK
}
} ;
void f()
{ // クラス外
Base base ;
base.private_member ; // エラー
base.protected_member ; // エラー
base.public_member ; // OK
}
class Derived : public Base
{
void f()
{
// 派生クラスのメンバー
private_member ; // エラー
protected_member ; // OK
public_member ; // OK
}
} ;
classキーワードで定義されたクラスのメンバーは、デフォルトでprivateになる。structキーワードで定義されたクラスのメンバーは、デフォルトでpublicになる。
// nameはprivate
class C { int name ; } ;
// nameはpublic
struct S { int name ; } ;
classとstructキーワードの違いは、デフォルトのアクセス指定子が異なるだけである。アクセス指定子を明示的に記述すると、classとstructキーワードの違いはなくなる。
アクセス指定は、メンバーの種類を問わず、名前に対して一律に適用される。アクセス指定は、名前探索に影響をあたえることはない。たとえアクセス指定によって使えない名前であっても、名前は発見される。名前が発見されること自体はエラーではない。アクセス指定によって使えない名前を使おうとするとエラーになる。例えば、名前が関数のオーバーロードのセットであった場合、オーバーロード解決された結果の名前に対し、アクセス指定が適用される。
class X
{
private :
void f( int ) { }
public :
void f( double ) { }
} ;
int main()
{
X x ;
// エラー、privateメンバーにはアクセス出来ない
// オーバーロード解決の結果はX::f( int )
x.f( 0 ) ;
// OK、X::f( double )が呼ばれる
x.f( 0.0 ) ;
}
この例では、Xのメンバーfに対して、f(0)という関数呼び出しの式を適用している。アクセス指定は名前探索に影響をあたえることはないので、関数オーバーロードのセットとして、X::f(int)と、X::f(double)という名前が発見される。そして、オーバーロード解決によって、X::f(int)が最適な関数として選ばれる。アクセス指定のチェックは、オーバーロード解決の後に行われる。この場合、X::f(int)はprivateメンバーなので、Xのメンバーでもfriend関数でもないmain関数から呼び出すことはできない。
クラスのメンバーのアクセス指定は、ラベルにアクセス指定子(Access specifiers)を記述することで指定する。
アクセス指定子 : メンバー指定opt
アクセス指定 :
private
protected
public
アクセス指定子とは、private、protected、publicのいずれかである。アクセス指定子が現れた場所から、次のアクセス指定子か、クラス定義の終了までの間のメンバーが、アクセス指定子の影響を受ける。
class X
{
int a ; // デフォルトのprivate
public :
int b ; // public
int c ; // public
protected :
int d ; // protected
private :
int e ; // private
} ;
アクセス指定子には、順番や使用可能な回数の制限はない。好きな順番で、何度でも指定できる。
class X
{
public :
public :
protected :
public :
public :
private :
} ;
あるクラスを、別のクラスの基本クラスとするとき、いずれかのアクセス指定子を指定する。
class Base { } ;
class Derived_by_public : public Base { } ; // public派生
class Derived_by_protected : protected Base { } ; // protected派生
class Derived_by_private : private Base { } ; // private派生
アクセス指定子がpublicの場合、基本クラスのpublicメンバーは、派生クラスのpublicメンバーとしてアクセス可能になり、基本クラスのprotectedメンバーは、派生クラスのprotectedメンバーとしてアクセス可能になる。
class Base
{
public :
int public_member ;
protected :
int protected_member ;
} ;
class Derived : public Base
{
void f()
{
public_member ; // OK
protected_member ; // OK
}
} ;
int main()
{
Derived d ;
d.public_member ; // OK
}
アクセス指定子がprotectedの場合、基本クラスのpublicとprotectedメンバーは、派生クラスのprotectedメンバーとしてアクセス可能になる。
class Base
{
public :
int public_member ;
protected :
int protected_member ;
} ;
class Derived : protected Base
{
void f()
{
public_member ; // OK、ただしprotectedメンバー
protected_member ; // OK
}
} ;
int main()
{
Derived d ;
d.public_member ; // エラー、Derivedからは、protectedメンバーである
}
アクセス指定子がprivateの場合、基本クラスのpublicとprotectedメンバーは、派生クラスのprivateメンバーとしてアクセス可能になる。
class Base
{
public :
int public_member ;
protected :
int protected_member ;
} ;
class Derived : private Base
{
void f()
{
public_member ; // OK、ただし、privateメンバー
protected_member ; // OK、ただし、privateメンバー
}
} ;
class Derived2 : public Derived
{
void f()
{
public_member ; // エラー、基本クラスのprivateメンバーにはアクセスできない
protected_member ; // エラー、基本クラスのprivateメンバーにはアクセスできない
}
} ;
int main()
{
Derived d ;
d.public_member ; // エラー、Derivedからは、privateメンバーである
}
基本クラスにアクセス指定子を指定しなかった場合、structキーワードで宣言されたクラスは、デフォルトでpublicに、classキーワードで宣言されたクラスは、デフォルトでprivateになる。
struct Base { } ;
// デフォルトのpublic派生
struct D1 : Base { } ;
// デフォルトのprivate派生
class D2 : Base { } ;
どのアクセス指定子を指定して派生しても、基本クラスのprivateメンバーを派生クラスから使うことはできない。クラスAからprivate派生したクラスBから派生しているクラスCでは、クラスAのメンバーは使えないのも、この理由による。
// classキーワードで宣言されたクラスのメンバーはデフォルトでprivate
class Base { int private_member ; } ;
class Derived : public Base
{
// どのアクセス指定を用いても、基本クラスのprivate_memberは使えない
} ;
struct A { int public_member ; } ;
class B : private A { } ;
class C : public B
{
// クラスBは、クラスAからprivate派生しているため、ここではA::public_memberは使えない。
} ;
クラス名自体も、クラススコープ内の名前として扱われる。クラスAからprivate派生したクラスBから派生しているクラスCでは、クラスAのクラス名自体がprivateメンバーになってしまう。
// グローバル名前空間のスコープ
struct A { } ;
class B : private A { } ;
class C : public B
{
void f()
{
A a1 ; // エラー、名前Aは、基本クラスのprivateメンバーのA
::A a2 ; // OK、名前::Aは、グローバル名前空間スコープ内のA
}
} ;
この例では、クラスCのスコープ内で、非修飾名Aに対して、クラス名Aが発見されてしまうので、エラーになる。クラスCの中でクラスAを使いたい場合、明示的な修飾が必要である。
アクセス指定子は、staticメンバーにも適用される。publicなstaticメンバーを持つクラスを、protectedやprivateで派生すると、基本クラスからはアクセスできるが、派生クラスを介してアクセスできなくなってしまうこともある。
// グローバル名前空間のスコープ
struct A { static int data ; } ;
int A::data ;
class B : private A { } ;
class C : public B
{
void f()
{
data ; // エラー
::A::data ; // OK
}
} ;
クラスCからは、名前dataは、基本クラスAのメンバーdataとして発見されるので、アクセスできない。しかし、クラスA自体は、名前空間に存在するので、明示的な修飾を使えば、アクセスできる。
protectedの場合、friendではないクラス外部の関数からアクセスできなくなる。
struct A { static int data ; }
int A::data ;
class B : protected A { } ;
int main()
{
B::data ; // エラー
A::data ; // OK
}
ここでは、B::dataとA::dataは、どちらも同じオブジェクトを指しているが、アクセス指定の違いにより、B::dataという修飾名では、クラスBのfriendではないmain関数からアクセスすることができない。
基本クラスにアクセス可能である場合、派生クラスへのポインター型から、基本クラスへのポインター型に型変換できる。
class A { } ;
class B : public A { } ;
class C : protected A
{
void f()
{
static_cast< A * >( this ) ; // OK、アクセス可能
}
} ;
int main()
{
B b ;
static_cast< A * >( &b ) ; // OK、アクセス可能
C c ;
static_cast< A * >( &c ) ; // エラー、main関数からは、protectedメンバーにアクセスできない
}
クラスはfriendを宣言することができる。friendを宣言するには、friend指定子を使う。クラスのfriendとして宣言できるものは、関数かクラスである。クラスのfriendは、クラスのprivateとprotectedメンバーにアクセスできる。
class X
{
private :
typedef int type ; // privateメンバー
friend void f() ; // friend関数
friend class Y ; // friendクラス
} ;
void f()
{
X::type a ; // OK、関数void f(void)はXのfriend
}
class Y
{
X::type member ; // OK、クラスYはXのfriend
void f()
{
X::type member ; // OK、クラスYはXのfriend
}
} ;
friendクラスの宣言は、friend指定子に続けて、複雑型指定子、単純型指定子、typename指定子(名前解決を参照)のいずれかを宣言しなければならない。
複雑型指定子は、最も分かりやすい。
class X
{
friend class Y ;
friend struct Z ;
} ;
複雑型指定子を使う場合、クラスをあらかじめ宣言しておく必要はない。名前がクラスであることが、その時点で宣言されるからだ。
単純型指定子に名前を使う場合は、それより以前に、クラスを宣言しておく必要がある。
class Y ; // Yをクラスとして宣言
class X
{
friend Y ; // OK、Yはクラスである
friend Z ; // エラー、名前Zは見つからない
friend class A ; // OK、Aはクラスとして、ここで宣言されている
} ;
あらかじめ名前が宣言されていない場合は、エラーとなる。
単純型指定子にテンプレート名を使うこともできる。
template < typename T >
class X
{
friend T ; // OK
} ;
typename指定子を指定する場合は、以下のようになる。
template < typename T >
class X
{
friend typename T::type ;
} ;
T::typeは、依存名を型として使っているので、typenameが必要である。
もし、型指定子がクラス型ではない場合、単に無視される。これは、テンプレートコードを書くときに便利である。
template < typename T >
class X
{
friend T ;
} ;
X<int> x ; // OK、friend宣言は無視される
template < typename T >
class Y
{
friend typename T::type ;
} ;
struct Z { typedef int type ; } ;
Y<Z> y ; // OK、friend宣言は無視される
無視されるのは、あくまで、型指定子がクラス型ではなかった場合である。すでに説明したように、単純型指定子で、名前が見つからなかった場合は、エラーになる。
friend関数の宣言は、通常通りの関数の宣言の文法に、friend指定子を記述する。前方宣言は必須ではない。friend関数には、ストレージクラス指定子を記述することはできない。
class X
{
friend void f() ;
friend int g( int, int, int ) ;
friend X operator + ( X const &, X const & ) ;
} ;
friend関数として宣言された関数がオーバーロードされていた場合でも、friend関数として宣言したシグネチャの関数しか、friendにはならない。
void f( int ) ;
void f( double ) ;
class X
{
friend void f( int ) ;
} ;
この例では、void f(int)のみが、Xのfriend関数になる。void f(double)は、friend関数にはならない。
他のクラスのメンバー関数も、friend関数として宣言できる。メンバー関数には、コンストラクターやデストラクターも含まれる。
class X ; // 名前Xをクラス型として宣言
class Y
{
public :
void f( ) ; // メンバー関数
Y & operator = ( X const & ) ; // 代入演算子
Y() ; // コンストラクター
~Y() ; // デストラクター
} ;
class X
{
// 以下4行は、すべて正しいfriend宣言
friend void Y::f( ) ;
friend Y & Y::operator = ( X const & ) ;
friend Y::Y() ;
friend Y::~Y() ;
} ;
friend宣言自体には、アクセス指定は適用されない。ただし、friend宣言の中でアクセスできない名前を使うことはできない。
class Y
{
private :
void f( ) ; // privateメンバー
} ;
class X
{
// エラー、Yのprivateメンバーにはアクセス出来ない
// friend宣言の中の名前の使用には、アクセス指定が影響する
friend void Y::f() ;
// アクセス指定は、friend宣言自体に影響を及ぼさない
// 以下3行のfriend宣言に、アクセス指定は何の意味もなさない
private :
friend void f() ;
protected :
friend void g() ;
public :
friend void h() ;
} ;
Y::fはprivateメンバーなので、Xからはアクセスできない。Xのfriend宣言は、関数f, g, hを、Xのfriendとして宣言しているが、この宣言に、Xのアクセス指定は何の効果も与えない。
friend宣言は、実は関数を定義することができる。
class X
{
friend void f() { } // 関数の定義
} ;
friend宣言で定義された関数は、クラスが定義されている名前空間スコープの関数になる。クラスのメンバー関数にはならない。ただし、friend宣言で定義された関数は、ADLを使わなければ、呼び出すことはできない。非修飾名前探索や、修飾名前探索で、関数名を参照する方法はない。
// グローバル名前空間のスコープ
class X
{
// fはメンバー関数ではない
// クラスXの定義されているグローバル名前空間のスコープ内の関数
friend void f( X ) { }
// gはメンバー関数ではない
// gを呼び出す方法は存在しない
friend void g() { }
} ;
int main()
{
X x ;
f(x) ; // OK、ADLによる名前探索
(f)(x) ; // エラー、括弧がADLを阻害する。ADLが働かないので名前fが見つからない
::f(x) ; // エラー、名前fが見つからない
g() ; // エラー、名前gが見つからない
}
このように、通常の名前探索では関数名が見つからないという問題があるため、friend宣言内での関数定義は、行うべきではない。
friendによって宣言された関数は、前方宣言されていない場合、外部リンケージを持つ。前方宣言されている場合、リンケージは前方宣言に従う。
inline void g() ; // 前方宣言、関数gは内部リンケージを持つ
class X
{
friend void f() ; // 関数fは外部リンケージを持つ
friend void g() ; // 関数gは内部リンケージを持つ
} ;
// 定義
void f() { } // 外部リンケージ
inline void g() { }
friend宣言は、派生されることはない。また、あるクラスのfriendのfriendは、あるクラスのfriendではない。つまり、友達の友達は、友達ではない。
class A
{
private :
typedef int type ;
friend class B ;
} ;
class B
{
// OK、BはAのfriend
typedef A::type type ;
friend class C ;
} ;
class C
{
// エラー、BはAのfriendである。CはBのfriendである。
// Cは、Aからみて、friendのfriendにあたる。
// しかし、CはAのfriendではない。
typedef A::type type ;
} ;
class D : public B
{
// エラー、DはBから派生している。BはAのfriendである。
// しかし、DはAのfriendではない
typedef A::type type ;
} ;
ローカルクラスの中でfriend宣言で、非修飾名を使った場合、名前探索において、ローカルクラスの定義されている関数外のスコープは考慮されない。friend関数を宣言する場合、対象の関数はfriend宣言に先立って宣言されていなければならない。friendクラスを宣言する場合、クラス名はローカルクラスの名前であると解釈される。
class A ; // ::A
void B() ; // ::B
void f()
{
// 関数の前方宣言は関数内でも可能
void C( void ) ; // 定義は別の場所
class Y ; // ローカルクラスYの宣言
// ローカルクラスXの定義
class X
{
friend class A ; // OK、ただし、::Aではなく、ローカルクラスのA
friend class ::A ; // OK、::A
friend class Y ; // OK、ただしローカルクラスY
friend void B() ; // エラー、Bは宣言されていない。::Bは考慮されない
friend void C() ; // OK、関数内の前方宣言により名前を発見
} ;
}
friend宣言とテンプレートの組み合わせについては、テンプレート宣言のfriendを参照。
protectedメンバーは、friendか、メンバーの宣言されたクラスか、クラスから派生しているクラスからアクセスすることができる。
void g() ;
class Base
{
friend void g() ;
protected :
int member ;
} ;
void f()
{
Base base ;
base.member ; // エラー、protectedメンバー
}
void g()
{
Base base ;
base.member ; // OK、gはBaseのfriend
}
class Derived : protected Base
{
void f()
{
member ; // OK、DerivedはBaseから派生している
}
} ;
virtual関数へのアクセスは、virtual関数の宣言によって決定される。virtual関数のオーバーライドには影響されない。
class Base
{
public :
virtual void f() { }
} ;
class Derived : public Base
{
private :
void f() { } // Base::fをオーバーライド
} ;
int main()
{
Derived d ;
d.f() ; // エラー、Derived::fはprivateメンバー
Base & ref = d ;
ref.f() ; // OK、Derived::fを呼ぶ
}
Derived::fはprivateメンバーなので、関数mainから呼び出すことはできない。しかし、Base::fはpublicメンバーである。Base::fはvirtual関数なので、呼び出す関数は、実行時のオブジェクトの型によって決定される。この時、オーバーライドしたvirtual関数のアクセス指定は、考慮されない。Base::fのアクセス指定のみが考慮される。この例では、関数mainから、Derived::fを直接呼び出すことはできないが、Baseへのリファレンスやポインターを経由すれば、呼び出すことができる。
virtual関数呼び出しのアクセスチェックは、呼び出す際の式の型によって、静的に決定される。基本クラスでpublicメンバーとして宣言されているvirtual関数を、派生クラスでprotectedやprivateにしても、基本クラス経由で呼び出すことができる。
多重派生によって、基本クラスのメンバーに対して、複数のアクセスパスが形成されている場合、アクセス可能なパスを経由してアクセスが許可される。
class Base
{
public :
void f() { }
} ;
class D1 : private virtual Base { } ;
class D2 : public virtual Base { } ;
class Derived : public D1, public D2
{
void f()
{
Base::f() ; // OK、D2を経由してアクセスする
}
} ;
D1はBaseをprivate派生しているので、DerivedからD1経由では、Baseにアクセスできない。しかし、D2経由でアクセスできる。
ネストされたクラスも、クラスのメンバーであるので、他のメンバーとアクセス権限を持つ。
class Outer
{
private :
typedef int type ; // privateメンバー
class Inner
{
Outer::type data ; // OK、InnerはOuterのメンバー
} ;
} ;
OuterにネストされたクラスInnerは、Outerのメンバーなので、Outerのprivateメンバーにアクセスすることができる。
ただし、ネストされたクラスをメンバーとして持つクラスは、ネストされたクラスに対して、特別なアクセス権限は持たない。
class Outer
{
class Inner
{
private :
typedef int type ; // privateメンバー
} ;
void f()
{
Inner::type x ; // エラー、Inner::typeはprivateメンバー
}
} ;
この例では、Outerは、Innerのprivateメンバーにはアクセスできない。
クラスのメンバー関数の中でも、特別な扱いを受けるメンバー関数が存在する。デフォルトコンストラクター、コピーコンストラクター、コピー代入演算子、ムーブコンストラクター、ムーブ代入演算子、デストラクターは、特別なメンバー関数(special member functions)である。
これらのメンバー関数を明示的に定義しない場合、暗黙のメンバー関数が生成される。暗黙に生成された特別なメンバー関数は、明示的に使用することもできる。
#include <utility>
struct X { } ;
int main()
{
X x1 ; // デフォルトコンストラクター
X x2( x1 ) ; // コピーコンストラクター
X x3( std::move(x1) ) ; // ムーブコンストラクター
x2 = x1 ; // コピー代入演算子
x2 = std::move( x1 ) ; // ムーブ代入演算子
x2.operator=( x1 ) ; // メンバー関数の明示的な使用、x2 = x1と同等
}
クラスXは、特別なメンバー関数を一切定義していない。しかし、特別なメンバー関数は、暗黙のうちに生成されるので、クラスXのオブジェクトを初期化、破棄できるし、コピーやムーブもできる。
ユーザー定義されていない特別なメンバー関数は、条件次第で、暗黙にdelete定義されることもある。そのようなクラスの特別なメンバー関数を使いたい場合は、ユーザー定義しなければならない。
struct X
{
int & member ;
// デフォルトコンストラクターは暗黙にdelete定義される
// X() = delete ; が暗黙に宣言される
} ;
int data ; // グローバル変数
struct Y
{
int & member ;
Y() : member(data) { }
} ;
int main()
{
X x ; // エラー、デフォルトコンストラクターはdelete定義されている
Y y ; // OK、ユーザー定義のデフォルトコンストラクターがある
}
特別なメンバー関数を明示的に宣言、定義することで、クラスをどのように生成、破棄、コピー、ムーブするかを記述できる。また、これらの操作を、明示的に禁止することもできる。
struct X
{
// コピー、ムーブコンストラクターをdelete定義
X( X const & ) = delete ;
X( X && ) = delete ;
// コピー、ムーブ代入演算子をdelete定義
X & operator = ( X const & ) = delete ;
X & operator = ( X && ) = delete ;
} ;
この例では、コピーもムーブもできないクラスを定義している。
特別なメンバー関数も、アクセス指定の影響を受ける。
class X
{
public :
X( int ) { }
private :
X( double ) { }
} ;
int main()
{
X a( 0 ) ; // OK、X::X(int)はpublicメンバー
X b( 0.0 ) ; // エラー、X::X(double)はprivateメンバー
}
この例では、X::X(int)はpublicメンバーなので、main関数からアクセスできるが、X::X(double)は、privateメンバーなので、main関数からアクセスできない。その結果、変数bの定義はエラーとなる。
コンストラクターは名前を持たない。コンストラクターの宣言には、特別な文法が用いられる。
関数指定子もしくはconstexpr クラス名 仮引数リスト
struct X
{
X() ; // コンストラクターの宣言
} ;
X::X() { } // コンストラクターの定義
コンストラクターは、クラス型のオブジェクトを初期化するのに用いられる。
コンストラクターのクラス名に、typedef名を用いることはできない。
コンストラクターに、virtual指定子、static指定子を指定することはできない。要約すれば、コンストラクターに使用可能な指定子は、inline、explicit、constexprである。コンストラクターをCV修飾することはできない。ただし、コンストラクターは、CV修飾されたオブジェクトの初期化に対しても、呼び出される。オブジェクトのCV修飾子は、構築中のオブジェクトには適用されないからである。コンストラクターをリファレンス修飾することはできない。
struct X {
virtual X() ; // エラー、コンストラクターにvirtual指定子は使えない
static X() ; // エラー、コンストラクターにstatic指定子は使えない
X() const ; // エラー、コンストラクターはCV修飾できない
X() & ; // エラー、コンストラクターはリファレンス修飾できない
} ;
デフォルトコンストラクター
実引数なしで呼べるコンストラクターを、デフォルトコンストラクター(default constructor)という。これには、仮引数を取らないコンストラクターの他に、仮引数にすべてデフォルト実引数が指定されているコンストラクターも含まれる。
以下はすべて、Xに対するデフォルトコンストラクターの宣言である。
X() ;
X( int = 0 ) ;
X( int = 0, int = 0 ) ;
もし、ユーザー定義のコンストラクターが存在しない場合、暗黙のデフォルトコンストラクターが、デフォルト化されて宣言される。
struct X
{
// デフォルトコンストラクターの定義なし
// 暗黙に、X() = default ; が宣言される
} ;
struct Y
{
Y(int) ; // ユーザー定義のコンストラクター
// 暗黙のデフォルトコンストラクターはdefault化されない
} ;
ただし、以下のいずれかの条件をみたすクラスの場合、暗黙のデフォルトコンストラクターはdelete定義される。
unionのようなクラスで、共用メンバーが非トリビアルデフォルトコンストラクターを持つ場合
// 非トリビアルデフォルトコンストラクターを持つクラス
struct NonTrivial
{
NonTrivial() { } // 非トリビアルデフォルトコンストラクター
} ;
// 非トリビアルデフォルトコンストラクターを持つメンバーのあるunion
union X
{
// 暗黙のデフォルトコンストラクターはdelete定義される
NonTrivial nt ;
} ;
// そのような無名unionを直接のメンバーに持つクラス
struct Y
{
// 暗黙のデフォルトコンストラクターはdelete定義される
union { NonTrivial nt ; } ;
} ;
初期化子のないリファレンス型の非staticデータメンバーを持つクラスの場合
int OBJECT ; // グローバル変数
struct X
{
// 初期化子のないリファレンス型の非staticデータメンバー
int & ref ;
} ;
struct Y
{
// 初期化子がある
int & ref = OBJECT ;
} ;
struct Z
{
int & ref ;
// ユーザー定義のコンストラクターがある
Z() : ref( OBJECT ) { }
} ;
int main()
{
X x ; // エラー、デフォルトコンストラクターがdelete定義される
Y y ; // OK
Z z ; // OK
}
クラスXのデフォルトコンストラクターは暗黙にdelete定義される。Y::refには初期化子がある。Zにはユーザー定義のコンストラクターがある。
unionのメンバーではない、const修飾された型かあるいはその配列型の、非staticデータメンバーが、ユーザー定義デフォルトコンストラクターを持たず、初期化子もない場合。
// ユーザー定義デフォルトコンストラクターを持たないクラス
struct NoUserDefined { } ;
struct X
{
// 初期化子がない
NoUserDefined const member ;
NoUserDefined const array[1] ;
} ;
struct Y
{
// 初期化子がある
NoUserDefined const member = NoUserDefined() ;
} ;
struct Z
{
NoUserDefined const member ;
// memberに対する初期化子がないので不可
// Z() : member() { } なら可
Z() { }
} ;
int main()
{
X x ; // エラー
Y y ; // OK
Z z ; // エラー
}
気をつける点としては、Zのユーザー定義デフォルトコンストラクターが、Z(){}という形の場合、Z::memberに対する初期化子がないので、エラーになる。Z() : member() { }という形の場合は、初期化子があるので、エラーにはならない。
unionのようなクラスで、共用メンバーがconst修飾されている場合。これには、const修飾されている型への配列型も含む。
union X
{ // すべてconst修飾されている
int const a ;
int const b ;
int const c[1] ;
} ;
struct Y
{ // すべてconst修飾されている無名unionをメンバーに持つ
X x ;
} ;
union Z
{
int a ; // const修飾されていない
int const b ;
int const c[1] ;
} ;
int main()
{
X x ; // エラー
Y y ; // エラー
Z z ; // OK、Y::aはconst修飾されていない。
}
unionのすべての非staticデータメンバーがconst修飾されている場合のみ、デフォルトコンストラクターが暗黙にdelete定義される。ひとつでもconst修飾されていない非staticデータメンバーがある場合、この条件には当てはまらない。
直接の基本クラス、仮想基本クラス、初期化子のない非staticデータメンバーの型が、デフォルトコンストラクターが使えない型である場合。これには、配列型も含まれる。
// デフォルトコンストラクターが使えないクラスの例
struct X
{
X() = delete ;
} ;
// 直接の基本クラス
struct A : X { } ;
// 仮想基本クラス
struct B : virtual X { } ;
// 初期化子のない非staticデータメンバー
struct C
{
X a ;
X b[1] ;
} ;
クラスA、B、Cは、いずれもデフォルトコンストラクターが暗黙にdelete定義される。
ある型のデフォルトコンストラクターが使えない場合というのは、以下の通りである。
デフォルトコンストラクターがない場合。
// ユーザー定義コンストラクターがある場合、暗黙のデフォルトコンストラクターは定義されない。
struct NoDefaultConstructor { NoDefaultConstructor(int) ; } ;
struct X : NoDefaultConstructor { } ;
デフォルトコンストラクターのオーバーロード解決の結果が曖昧になる場合。
struct Ambiguous
{
Ambiguous( int = 0 ) { }
Ambiguous( double = 0.0 ) { }
} ;
struct X : Ambiguous { } ;
int main()
{
Ambiguous a ; // エラー、デフォルトコンストラクターのオーバーロード解決が曖昧
X b ; // エラー、デフォルトコンストラクターは暗黙にdelete定義されている
}
デフォルトコンストラクターがdelete定義されている場合
struct X
{
X() = delete ; // デフォルトコンストラクターのdelete定義
} ;
クラスのデフォルト化されたデフォルトコンストラクターから、ある型のデフォルトコンストラクターにアクセス出来ない場合。
class B1
{
private :
B1() = default ;
} ;
class B2
{
private :
B2() { }
} ;
class D1 : public B1 { } ;
class D2 : public B2 { } ;
int main()
{
D1 a ; // エラー、デフォルトコンストラクターは暗黙にdelete定義されている
D2 b ; // エラー、デフォルトコンストラクターは暗黙にdelete定義されている
}
クラスB1、B2のデフォルトコンストラクターは、privateメンバーなので、friendではないクラスD1、D2からはアクセスできない。そのため、デフォルトコンストラクターは暗黙にdelete定義される。
これらの条件に当てはまらない場合、デフォルトコンストラクターは暗黙にdefault化される。
デフォルトコンストラクターがトリビアル(trivial)となるためには、以下の条件をすべて満たさなければならない。
デフォルトコンストラクターがユーザー定義もdelete定義もされていない。クラスはvirtual関数とvirtual基本クラスを持たない。クラスの非staticデータメンバーは、初期化子を持たない。クラスの直接の基本クラスは、トリビアルデフォルトコンストラクターを持つ。クラスの非staticデータメンバーは、トリビアルデフォルトコンストラクターを持つ。
デフォルトコンストラクターは、使われたときに、暗黙にdefault化される。デフォルトコンストラクターがdelete定義されずに、default化された場合、暗黙に定義される。この暗黙のデフォルトコンストラクターは、コンストラクター初期化子を書かずコンストラクターの本体を空にした、ユーザー定義のデフォルトコンストラクターと同等である。
struct X
{
// 暗黙のデフォルトコンストラクターは、以下のコードと同じ
// X(){}
} ;
struct Y { } ;
int main()
{
X x ; // 暗黙のデフォルトコンストラクターを使う
}
クラスXはデフォルトコンストラクターが使われているので、暗黙にdefault化される。クラスYのデフォルトコンストラクターは使われていないので、定義されない。
もし、暗黙のデフォルトコンストラクターが定義されていて、同等のユーザー定義のデフォルトコンストラクターを書いた場合にエラーとなる場合は、プログラムもエラーとなる。
struct X { int & ref ; } ;
struct Y { int & ref ; } ;
int main()
{
X x ; // エラー
int obj = 0 ;
Y y = { obj } ; // OK
}
クラスXはデフォルトコンストラクターを使っているので、暗黙のデフォルトコンストラクターがdefault化される。しかし、リファレンスの非staticデータメンバーを持つことにより、同等のユーザー定義のデフォルトコンストラクターがエラーになるので、エラーとなる。一方、クラスYでは、デフォルトコンストラクターが使われていない。
同等のユーザー定義のデフォルトコンストラクターがconstexprコンストラクターの要求を満たす場合、暗黙のデフォルトコンストラクターも、constexprコンストラクターになる。
デフォルトコンストラクターは、初期化子なしで定義されたオブジェクトや、関数形式の明示的なキャストに対して呼ばれる。
struct X { X() { } } ;
X x ; // デフォルトコンストラクターが呼ばれる
int main()
{
X x ; // デフォルトコンストラクターが呼ばれる
new X ; // デフォルトコンストラクターが呼ばれる
X() ; // デフォルトコンストラクターが呼ばれる
}
クラスのオブジェクトをコピー、ムーブする際には、コピー、ムーブコンストラクターがそれぞれ使われる。詳しくは、クラスオブジェクトのコピーとムーブを参照。
基本クラスと非staticデータメンバーのコンストラクターが呼ばれる順番や、実引数の渡し方については、基本クラスとデータメンバーの初期化を参照。
その他のコンストラクターについては、型変換コンストラクターを参照。
コンストラクターに戻り値の型を指定することはできない。コンストラクターの本体の中でreturn文を使う場合は、値を指定してはならない。コンストラクターのアドレスを取得することはできない。
constなオブジェクトの構築中に、コンストラクターのthisを、直接、間接的に経由しないglvalueによってオブジェクト、またはそのサブオブジェクトにアクセスした場合、値は未規定である。この制限には、通常、まず遭遇することはない。例えば、以下のようなコードが問題になる。
struct C ;
void f( C * ) ;
struct C
{
int value ;
C() : c(123)
{ // オブジェクトは、まだ構築中
f( this ) ; // オブジェクトの構築中に呼び出す
}
} ;
const C cobj ; // staticストレージ上のconstなオブジェクト
void f( C* cptr )
{
cptr->value ; // OK、値は123。cptrはコンストラクターのthis由来
cobj.value ; // 値は未規定
}
オブジェクトは、コンストラクターを実行し終わった時点で、構築済みとなる。コンストラクターを実行中ということは、まだオブジェクトは構築中ということである。cobjは、デフォルトコンストラクターを呼び出す。したがって、関数fは、cobjの構築中に呼び出されるということになる。cptrの値は、コンストラクターのthisによって得られたアドレスである。したがって、cptrの値である、Cのオブジェクトへのアドレスを参照して、cobjにアクセスすることはできる。関数fは、クラスCのコンストラクターから呼び出されている。関数fの中からcobjを直接参照するということは、cobjの構築中に、コンストラクターのthisによらずにアクセスするということである。この場合、値は未規定となる。この例では、123であるとは保証されない。
この条件に当てはまるようなコードは、現実には極めて珍しい。
一時オブジェクト(temporary object)は、様々な場面で、自動的に生成、破棄される。例えば、prvalueをリファレンスに束縛する、prvalueを返す、prvalueを生成する型変換、例外のthrow、ハンドラーでキャッチ、初期化などである。例外における一時オブジェクトの寿命は、例外を参照。
struct X { } ;
X f()
{
return X() ; // prvalueを生成する型変換
}
int main()
{
int && ref = 0 ; // prvalueをリファレンスに束縛する
f() ; // prvalueを返す
}
実装は一時オブジェクトの生成を省略できる。例えば、以下のコードについて考える。
struct X
{
X( int ) ; // コンストラクター
X( X const & ) ; // コピーコンストラクター
X & operator = ( X const & ) ; // コピー代入演算子
~X() ; // デストラクター
} ;
struct Y
{
Y( int ) ; // コンストラクター
Y( Y && ) ; // ムーブコンストラクター
~Y() ; // デストラクター
};
X f( X ) ;
Y g( Y ) ;
int main()
{
X a = f( X(2) ) ; // #1
Y b = g( Y(3) ) ; // #2
X c(1) ;
c = f(c) ; // #3
}
#1について考える。ある実装では、X(2)という式で一時オブジェクトがひとつ作られ、関数の実引数として渡す際に、別の一時オブジェクトがひとつ作られてコピーされるかもしれない。関数の戻り値も、別の一時オブジェクトがひとつ作られて、変数aにコピーされるかもしれない。別の実装では、X(2)という式による一時オブジェクトは、関数の実引数の一時オブジェクト上に直接構築されるので、一時オブジェクトを省略できるかもしれない。関数の戻り値も、変数aのオブジェクト上に直接構築されるので、一時オブジェクトを省略できるかもしれない。
#2も、コピーがムーブに、変数aがbに変わっただけで、同じことが言える。
#3では、変数cは、すでに構築されたオブジェクトなので、関数の戻り値の一時オブジェクトを、変数cのオブジェクトの上に、直接構築することはできない。ここでは一時オブジェクトが構築され、変数cにコピーされる。
一時オブジェクトの構築が省略されたとしても、もし一時オブジェクトを作成していればエラーになるようなコードは、エラーになる。たとえば、コンストラクターやデストラクターにアクセスできない場合だ。
一般に、ある式において一時オブジェクトがいくつ構築されるかということは、規格では定義されていない。実装次第である。一時オブジェクトが構築されなければ、コンストラクターやデストラクターも呼ばれない。
逆に、一時オブジェクトが構築される場合、非トリビアルなコンストラクターは必ず呼び出されるし、破棄するときには、非トリビアルなデストラクターは必ず呼び出される。
一時オブジェクトの破棄は原則として、その一時オブジェクトを構築することになった式を含む完全式の評価の最後の段階として、実行される。言いかえれば、一時オブジェクトの寿命は、完全式が評価され終わるまでということもできる。
struct X
{
X operator + ( X const & ){ return X() ; }
} ;
int main()
{
X a = 0 ;
X b = a + a + a ;
}
この例で、bの初期化子の中の式において構築された一時オブジェクトがもしあれば、その寿命は、ソースコード上で言えば、セミコロンまでとなる。
ただし、この原則に従わない場合が、ふたつ存在する。
ひとつは、配列の要素を初期化する際に、デフォルトコンストラクターがデフォルト実引数を持っていた場合、ある要素のデフォルトコンストラクター実行における一時オブジェクトの破棄は、次の要素の初期化の前に行われる。破棄に伴うあらゆるサイドエフェクトは、次の要素の初期化の以前にシーケンス(sequenced before)される。
struct Fat { char [1000] ; } ;
struct X
{
X( Fat f = Fat() ) { } // デフォルトコンストラクター
} ;
int main()
{
X a[1000] ;
}
この例では、a[0]からa[999]までの1000個のX型の配列の要素に対し、デフォルトコンストラクターが呼び出される。もし、a[0]がa[1]の前に初期化された場合、a[0]のデフォルトコンストラクター呼び出しによって構築されたFat型の一時オブジェクトは、a[1]を初期化するときには、すでに破棄されている。配列のすべての要素を初期化し終わるまで、1000個のFat型の一時オブジェクトが保持されることはない。
もうひとつは、一時オブジェクトをリファレンスに束縛した場合、一時オブジェクトの寿命は、リファレンスの寿命まで延長される。一時オブジェクトは、rvalueリファレンスか、constなlvalueリファレンスで束縛できる。
struct X { } ;
int main()
{
{
X const & lvalue_reference = X() ;
X && rvalue_reference = X() ;
// 一時オブジェクトの寿命はここまで
}
}
ただし、このリファレンス束縛の寿命の延長には、いくつかの例外が存在する。
コンストラクター初期化子によってリファレンスのメンバーに束縛された一時オブジェクトの寿命は、コンストラクター呼び出しが終了するまでである。
struct Member { } ;
struct X
{
Member const & ref ;
X( Member const & ref ) : ref(ref)
{
// refは妥当なオブジェクトを参照している
}
} ;
int main()
{
X x = Member() ;
// 一時オブジェクトが破棄される
// x.refは無効なオブジェクトを参照している
}
もし、クラスXのコンストラクターの実引数が、一時オブジェクトへのリファレンスだった場合、一時オブジェクトの寿命は、コンストラクター呼び出しが終了するまでである。そのため、初期化の終わった変数xのメンバーrefは、無効なオブジェクトを参照していることになる。
仮引数のリファレンスに束縛された一時オブジェクトの寿命は、関数呼び出しを含む式の完全式の評価が終了するまでである。
struct X { } ;
void f( X const & ref )
{
// refは妥当なオブジェクトを参照している
}
int main()
{
f( X() ) ;
// 一時オブジェクトが破棄される
}
この例で、X()を含む完全式というのは、関数fに対する関数呼び出し式のオペランドである。したがって、完全式はX()となる。しかし、この解釈に従うと、関数の本体では、refは無効なオブジェクトを参照することになってしまう。そのため、仮引数のリファレンス束縛に対しては、関数呼び出しを含む完全式になる。この場合、f( X() )である。そのため、X()によって構築された一時オブジェクトは、関数fの本体の中でも妥当である。
関数のreturn文によって構築された、関数の戻り値のリファレンスに束縛された一時オブジェクトの寿命は、延長されない。一時オブジェクトは、return文を含む完全式の終了をもって、破棄される。
struct X { } ;
X const & f( X const & ref )
{
return X() ;
}
int main()
{
X const & ref = f( X() ) ;
// 一時オブジェクトは破棄される
// refは無効なオブジェクトを参照している
}
このように、関数の戻り値としてのリファレンスに束縛されても、一時オブジェクトの寿命は延長されない。これには注意が必要である。
new初期化子の中でリファレンス束縛された一時オブジェクトの寿命は、new初期化子を含む完全式の終わりまでである。
struct X { int const & ref ; } ;
int main()
{
X * ptr = new X{ 0 } ;
// 一時オブジェクトが破棄される
// ptr->refは無効なオブジェクトを参照している
}
リファレンス束縛により、寿命の延長を受けていない一時オブジェクトの破棄の順番は、構築の逆順に行われる。後に構築された一時オブジェクトの方が、先に構築された一時オブジェクトより、先に破棄される。
もし、リファレンス束縛を受けた、複数の一時オブジェクトが、同じ場所で破棄される場合、破棄の順番は、構築の逆順に行われる。
struct X { X( int ) { } } ;
void f( X const &, X const & ) { }
int main()
{
f( X(1), X(2) ) ; // #1
}
今、#1のX(1)、X(2)という式に対して、それぞれ一時オブジェクトが構築されたとする。関数の実引数の評価順序は規定されていないので、どちらが先に構築されるかは、規格の定めるところではない。しかし、仮にX(1)の一時オブジェクトが、X(2)に先んじて構築された場合、オブジェクトの破棄は、X(2)が先になる。
クラスの型変換を実現するには、方法がふたつある。コンストラクターと変換関数(conversion function)だ。このふたつを合わせて、ユーザー定義型変換(user-defined conversions)という。ユーザー定義型変換は、暗黙の型変換、初期化、明示的な型変換に用いられる。
ひとつの値に対して、ユーザー定義型変換は1回しか適用されない。
struct X { } ;
struct Y
{
Y( int ){ } // int型からY型へ
operator X() { return X() ; } // Y型からX型へ
} ;
int main()
{
Y a = 0 ; // OK、int型からY型への型変換
X b = Y(0) ; // OK、Y型からX型への型変換
X c = 0 ; // エラー
}
ユーザー定義型変換によって、int型からY型に変換することはできる。また、Y型からX型に変換することはできる。ただし、int型から、暗黙にX型に変換することはできない。なぜならば、それにはユーザー定義型変換を、2回適用しなければならないからだ。
ユーザー定義型変換は、曖昧にならない場合のみ、暗黙に使われる。派生クラスの型変換関数は、基本クラスの型変換関数を隠さない。ただし、同じ型に対する型変換関数の場合を除く。複数の型変換関数がある場合、関数のオーバーロード解決と同じ方法で、最適な関数が解決される。
struct Base
{
operator int() { return 0 ; }
} ;
struct Derived : Base
{
// Base::operator intを隠さない
operator char() { return char(0) ; }
} ;
int main()
{
Derived obj ;
int a = obj ; // OK
char b = obj ; // OK
bool c = obj ; // エラー、曖昧
}
explicit指定子を使わずに宣言されているコンストラクターは、仮引数からクラスへの型変換の方法を指定するメンバー関数である。このような関数を、型変換コンストラクター(converting constructor)という。
struct X
{
X( int ) {} // int型からの型変換を提供
X( double ) {} // double型からの型変換を提供
X( int, int, int ) ; // 3個のint型からの型変換を提供
} ;
int main()
{
X a = 0 ; // int型からの型変換
X b(0) ; // int型からの型変換
X c = 0.0 ; // double型からの型変換
X d( 1, 2, 3 ) ;
}
型変換コンストラクターは、仮引数からクラス型への変換方法を指定する。仮引数は、複数でもよい。
explicit指定子のあるコンストラクターは、explicitのないコンストラクターとほぼ同じである。ただし、explicitコンストラクターは、直接初期化か、キャストが明示的に使われたときにしか、使われない。
struct X
{
explicit X( int ) {} // explicitコンストラクター
} ;
void f( X ) { }
int main()
{
X a = 0 ; // エラー
X b(0) ; // OK、直接初期化
X c = X(0) ; // OK、明示的なキャスト
X d = static_cast<X>(0) ; // OK、明示的なキャスト
f( 0 ) ; // エラー
f( static_cast<X>(0) ) ; // OK
}
デフォルトコンストラクター、コピーコンストラクター、ムーブコンストラクターにも、explicitを指定できる。これらの関数も、explicit指定子を指定しない場合、暗黙に使われる。
struct X
{
X() { }
explicit X( X const & ) {} // explicitコピーコンストラクター
} ;
int main()
{
X a ;
X b = a ; // エラー、コピー初期化(代入式とは違うことに注意)
X c( a ) ; // OK、直接初期化
}
デフォルトコンストラクターに対するexplicit指定子の有無は、以下のコード例のような違いをもたらす。
struct X
{
X() { }
} ;
struct Y
{
explicit Y() { }
} ;
int main( )
{
X x( { } ) ; // OK、非explicitデフォルトコンストラクター
Y y( { } ) ; // エラー、explicitデフォルトコンストラクター
}
実用上、気になるほどの違いはない。
以下のような文法で宣言されるメンバー関数を、型変換関数(Conversion function)という。
explicitopt operator 型識別子 ( )
型変換関数は、仮引数を取らず、戻り値の型を指定しない。型変換関数は、メンバーであるクラス型から、型識別子の型への型変換を提供する。
struct X
{
operator int() { return 0 ; }
} ;
int main()
{
X x ;
int i = x ; // 0
}
この例では、クラスXは、暗黙にint型の0に変換できるクラスとなる。
型変換関数の型は、「仮引数を取らず、型識別子の型を返す、メンバー関数」になる。
struct X
{
operator int() const { return 0 ; }
} ;
int main()
{
// 型はint (X::*)(void) const
int (X::*ptr)(void) const = &X::operator int ; // ポインターを得る
X x ;
(x.*ptr)() ; // 呼び出す
}
型識別子が、自分自身のクラス型(リファレンスも含む)、自分自身の基本クラス型(リファレンスも含む)、void型、またこれらの型にCV修飾子を付けた型の場合、型変換関数が使われることはない。これらの型変換には、標準型変換が用いられる。型変換関数は使われない。これらの型変換関数を宣言することはエラーではないが、使われることはない。
struct Base{ } ;
struct Derived : Base
{
// これらの型変換関数は、使われることはない
operator Derived () ; // 自分自身のクラス型
operator Derived & () ; // 自分自身のクラスへのリファレンス型
operator Derived const & () ; // 自分自身のクラスへのconstリファレンス型
operator Base () ; // 基本クラス型
operator void () ; // void型
} ;
int main()
{
Derived d ;
Base b = d ; // 標準型変換が使われる。型変換関数は使われない
}
型変換関数にexplicit指定子が指定されている場合、型変換関数は、直接初期化や明示的なキャストが使われなければ、呼び出されない。
struct A { } ;
struct B { } ;
struct X
{
operator A() { return A() ; }
explicit operator B() { return B() ; }
} ;
int main()
{
X x ;
A a1 = x ; // OK
A a2(x) ; // OK
A a3 = A(x) ; // OK
B b1 = x ; // エラー、コピー初期化
B b2(x) ; // OK、直接初期化
B b3 = B(x) ; // OK、明示的なキャスト
}
型変換関数の型識別子を、関数型と配列型にすることはできない。
struct X
{
operator void (void) () ; // エラー、関数型
operator int[1] () ; // エラー、配列型
} ;
その他の型には、特に制限はない。
struct Y { } ;
struct X
{
using pointer_type = void (*)(void) ;
using reference_type = int (&)[1] ;
operator pointer_type () ; // 関数ポインタ―型
operator reference_type () ; // 配列への参照型
operator Y() ; // 他のクラス型
} ;
型変換関数は継承される。
struct Base
{
operator int() { return 0 ; }
} ;
struct Derived : Base { } ;
int main()
{
Derived d ;
int i = d ; // Base::operator intを呼び出す
}
型変換関数はvirtual関数にできる。
struct Base
{
// ピュアvirtual関数
virtual operator int() = 0 ;
} ;
struct Derived : Base
{
// オーバーライド
virtual operator int() { return 0 ; }
} ;
型変換関数はstatic関数にはできない。
struct X
{
static operator int() ; // エラー
} ;
以下のような文法の宣言を、デストラクター(destructor)という。
関数指定子opt ~ クラス名 ( )
デストラクターの宣言は、~(チルダ)に続いて、クラス名、空の引数リストを指定する。関数指定子には、inlineとvirtualを指定できる。クラス名の代わりに、typedef名を使用することはできない。
struct X
{
~X() ; // デストラクターの宣言
} ;
デストラクターはクラス型のオブジェクトを破棄する際に使われる。デストラクターには、仮引数や戻り値の型を指定することはできない。デストラクターのアドレスを得ることはできない。デストラクターはstaticメンバーにはなれない。デストラクターは、CV修飾、リファレンス修飾できない。ただし、デストラクターは、CV修飾されたクラス型のオブジェクトに対しても呼び出される。
struct X
{
~X() { }
} ;
int main()
{
{
X x ;
// オブジェクト破棄、デストラクターが呼ばれる
}
X * ptr = new X ;
delete ptr ; // オブジェクト破棄、デストラクターが呼ばれる
}
デストラクターの宣言に例外指定がない場合は、暗黙のデストラクターと同等の例外指定が、暗黙に指定される。詳しくは、例外指定を参照。
クラスにユーザー宣言されたデストラクターがない場合、デストラクターは暗黙にdefault化されて宣言される。暗黙に宣言されたデストラクターは、クラスのinline publicメンバーである。
暗黙のデストラクターは、以下のいずれかの条件を満たしたとき、delete定義される。
unionのようなクラスで、共用メンバーが、非トリビアルデストラクターを持つ場合。
struct Trivial { } ;
struct NonTrivial { ~NonTrivial() { } } ;
// デストラクターは暗黙にdefault化される
union A1 { Trivial member ; } ;
struct A2 { union { Trivial member ; } ; } ;
// デストラクターは暗黙にdelete定義される
union B1 { NonTrivial member ; } ;
struct B2 { union { NonTrivial member ; } ; } ;
int main()
{
// OK、暗黙のデストラクターを使う
A1 a1 ; A2 a2 ;
// エラー、暗黙のデストラクターはdelete定義されている
B1 b1 ; B2 b2 ;
}
クラスの非staticデータメンバーのデストラクターがdelete定義されているか、デフォルトデストラクターからアクセス出来ない場合。
struct deleted_destructor
{
~deleted_destructor() = delete ;
} ;
struct inaccessible_destructor
{
private :
~inaccessible_destructor() ;
friend struct Y ;
} ;
// クラスのデストラクターは暗黙にdelete定義される
struct X
{
deleted_destructor m1 ; // デストラクターがdelete定義されている
inaccessible_destructor m2 ; // デストラクターにアクセス出来ない
} ;
struct Y
{
inaccessible_destructor m ; // friendなので、デストラクターにアクセス可能
} ;
直接の基本クラス、もしくは、virtual基本クラスのデストラクターがdelete定義されているか、デフォルトデストラクターからアクセス出来ない場合。
struct Base
{
~Base() = delete ;
} ;
struct D1 : Base
{
// D1のデストラクターはdelete定義される
} ;
間接の基本クラスのデストラクターは、影響しない。
struct Base
{
private :
~Base() { }
friend struct D1 ;
} ;
struct D1 : Base
{
// friend宣言により、Baseのデストラクターにアクセスできる
} ;
struct D2 : D1
{
// D1のデストラクターにアクセスできる
} ;
int main()
{
D1 d1 ; // OK
D2 d2 ; // OK
}
ただし、virtual基本クラスには、直接と間接の違いはないので、影響する。
struct Base
{
private :
~Base() { }
friend struct D1 ;
} ;
struct D1 : virtual Base
{
// friend宣言により、Baseのデストラクターにアクセスできる
} ;
struct D2 : D1
{
// virtual基本クラスのBaseのデストラクターにアクセスできない
// デストラクターは暗黙にdelete定義される
} ;
int main()
{
D1 d1 ; // OK、暗黙のデストラクターを使う
D2 d2 ; // エラー、デストラクターはdelete定義されている
}
D2からD1のデストラクターにアクセスすることはできるが、D2からvirtual基本クラスであるBaseのデストラクターにアクセス出来ないため、D2のデストラクターは暗黙にdelete定義される。
デストラクターがトリビアルとなるためには、ユーザー提供もdelete定義もされておらず、以下の条件をすべて満たす必要がある。
- デストラクターはvirtualではない。
- 直接の基本クラスのデストラクターは、すべてトリビアルである。
- 非staticデータメンバーのデストラクターは、すべてトリビアルである。
注意すべきこととしては、直接の基本クラスのデストラクターがトリビアルとなるためには、直接の基本クラスの直接の基本クラスのデストラクターもトリビアルでなければならない。つまり、最終的には、間接の基本クラスのデストラクターも、すべてトリビアルでなければならない。
delete定義されていない暗黙のデストラクターは、使われたときに、定義される。もしくは、明示的にdefault化されたときにも定義される。
デストラクターの呼び出しは、コンストラクター呼び出しの逆順に行われる。コンストラクター呼び出しの順番については、基本クラスとデータメンバーの初期化を参照。
クラスのデストラクターの本体の実行を終え、本体内の自動変数を破棄する。共用メンバーを除くクラスの直接のメンバーに対して、デストラクターを呼び出す。クラスの直接の基本クラスのデストラクターを呼び出す。クラスが、最上位の派生クラスならば、virtual基本クラスのデストラクターを呼び出す。
配列の要素に対するデストラクターも、コンストラクターの逆順に呼ばれる。
デストラクターは、virtual関数やピュアvirtual関数にすることができる。基本クラスのデストラクターがvirtual関数である場合、派生クラスのデストラクターもvirtualになる。基本クラスのデストラクターがピュアvirtual関数の場合、派生クラスのオブジェクトを構築するためには、デストラクターを定義しなければならない。これらは、通常のvirtual関数と変わらない。
struct Base
{
virtual ~Base() { } // デストラクターはvirtual関数
} ;
struct Derived : Base
{
~Derived() { } // デストラクターはvirtual関数
} ;
struct Abstract_base
{
virtual ~Abstract_base() = 0 ; // デストラクターはピュアvirtual関数
} ;
デストラクターをvirtual関数にする目的は、オブジェクトに動的に構築、破棄する際に、型情報を管理しなくてもいいという点にある。
#include <iostream>
struct B1
{
virtual ~B1() { } // virtual関数
} ;
struct D1 : B1
{
~D1() { std::cout << "D1 destructor" << std::endl ; }
} ;
struct B2
{
~B2() {} // 非virtual関数
} ;
struct D2 : B2
{
~D2() { std::cout << "D2 destructor" << std::endl ; }
} ;
int main()
{
B1 * b1_ptr = new D1 ;
delete b1_ptr ; // 派生クラスのデストラクターが呼ばれる
B2 * b2_ptr = new D2 ;
delete b2_ptr ; // 派生クラスのデストラクターが呼ばれない
}
delete式に渡しているのは、基本クラスへのポインターである。そのため、非virtualなデストラクターでは、派生クラスのデストラクターが呼び出されない。デストラクターをvirtual関数にしておけば、このような場合にも、派生クラスのデストラクターが正しく呼び出される。
デストラクターが暗黙に呼ばれる条件は、以下の通りである。
- staticストレージ上のオブジェクトに対しては、プログラムの終了時に呼ばれる。
- threadストレージ上のオブジェクトに対しては、スレッドの終了時に呼ばれる。
- 自動ストレージ上のオブジェクトに対しては、オブジェクトを構築したブロックを抜けたときに呼ばれる。
- 一時オブジェクトに対しては、寿命が尽きたときに呼ばれる。
- new式で構築されたオブジェクトに対しては、delete式で破棄されるときに呼ばれる
- その他、例外として投げられたオブジェクトのキャッチに関連して呼ばれることがある
クラス型、もしくはクラスの配列型のオブジェクトが宣言された箇所で、クラスのデストラクタにアクセス出来ない場合は、エラーとなる。
class X
{
private :
~X() { } // privateメンバー
} ;
int main()
{
X x ; // エラー、デストラクターにアクセスできない。
}
クラスがvirtualデストラクターを持つ場合、クラスには対応する解放関数が使える状態でなければならない。解放関数は、まずクラスのスコープ内で探され、見つからない場合は、グローバルスコープで探される。解放関数が見つからないか、曖昧か、delete定義されている場合、エラーとなる。これは、たとえプログラム中でdelete式を使わなくてもエラーとなる。
struct X
{
virtual ~X() { } // OK、グローバルスコープのoperator deleteが発見される
} ;
struct Y
{
virtual ~Y() { } // エラー、operator deleteはdelete定義されている。
void operator delete( void * ptr ) = delete ;
} ;
struct B1
{
void operator delete( void * ptr ) ;
virtual ~B1() { }
} ;
struct B2
{
void operator delete( void * ptr ) ;
} ;
struct Derived : B1, B2
{
// エラー、曖昧
// 暗黙のデストラクターはvirtual関数
} ;
この規格の意図は、動的な型のオブジェクトは、常にdelete式が適用できることを保証するためである。
デストラクターは、明示的に呼び出すことができる。デストラクターを明示的に呼び出すには、メンバーアクセス演算子を使い、~に続いて、クラス型に対応する型名か、decltype指定子を使う。
// このコードは、あくまで明示的なデストラクター呼び出しを説明するための例である
// 関数fを呼び出すと、Xのデストラクターは4回呼び出されることになり、挙動は未定義である
struct X { } ;
void f()
{
X x ;
x.~X() ; // デストラクターの明示的な呼び出し(型名)
x.~decltype(x)() ; // デストラクタの明示的な呼び出し(decltype指定子)
x.X::~X() ; // 修飾名付き
// ブロックを抜ける際に、デストラクターが暗黙に呼び出される
}
たとえ、自動ストレージ上のオブジェクトに対してデストラクターを明示的に呼び出したとしても、ブロックを抜けた際に、デストラクターは暗黙的に呼び出される。デストラクターを呼び出した後のオブジェクトに対して、再びデストラクターを呼び出した場合、挙動は未定義なので、上記のコードの挙動も、未定義である。
一般に、自動ストレージ上のオブジェクトに対して明示的にデストラクターを呼び出した後、そのオブジェクトに対して、通常ならば暗黙にデストラクターが呼び出される状況になっている場合、挙動は未定義である。
デストラクターの明示的な呼び出しは、まず使う必要はない。デストラクターが暗黙に呼び出されることがない場合としては、placement newによる、ユーザー指定のストレージ上へのオブジェクトの構築が挙げられる。
#include <new>
struct X
{
~X() { /*実装*/ }
} ;
int main()
{
void * ptr = ::operator new( sizeof(X) ) ; // ストレージを確保
X * x_ptr = new(ptr) X ; // ptrの指すストレージ上にX型のオブジェクトを構築
x_ptr->~X() ; // デストラクターの明示的な呼び出し
::operator delete( ptr ) ; // ストレージの解放
}
スカラー型に対しても、デストラクターを明示的に呼び出すことができる。これによって、テンプレートのコードにおいて、型が組み込み型であるかどうかを気にしなくてすむ。
int main()
{
typedef int I ;
I i ;
i.~I() ; // OK、なにもしない
}
デストラクターを呼び出した後のオブジェクトに対して、再びデストラクターを呼び出した場合の挙動は未定義である。
クラスに対する確保関数(operator new)と解放関数(operator delete)は、メンバー関数としてオーバーロードすることができる。確保関数と解放関数の具体的な実装方法については、動的メモリー管理を参照。
クラスのメンバー関数としての確保関数、解放関数は、staticメンバー関数である。たとえstatic指定子が明示的に使われていなくても、staticメンバー関数となる。
#include <cstddef>
struct X
{
// 確保関数
void * operator new ( std::size_t size )
{ return ::operator new( size ) ; }
// 配列
void * operator new[] ( std::size_t size )
{ return ::operator new( size ) ; }
// placement form
void * operator new ( std::size_t size, int, int, int )
{ return ::operator new( size ) ; }
// 解放関数
void operator delete( void * ptr )
{ ::operator delete ( ptr ) ; }
// 配列
void operator delete[] ( void * ptr )
{ ::operator delete ( ptr ) ; }
// placement form
void operator delete( void * ptr, int, int, int )
{ ::operator delete ( ptr ) ; }
} ;
解放関数に例外指定がない場合、noexcept(true)が指定されたものとみなされる。
この項目では、クラスのオブジェクトの初期化について取り扱う。特に、明示的に初期化子が指定されているオブジェクトと、クラスの基本クラスとメンバーのサブオブジェクトの初期化方法を解説する。
クラスのオブジェクトに初期化子が指定されていない場合の初期化方法は、初期化子を参照。
クラスオブジェクトの配列の要素を初期化する際には、コンストラクターは、添字の順番に呼ばれる。デストラクターはコンストラクターの逆順に呼ばれる。
#include <iostream>
class X
{
private :
int value ;
public :
X( int value ) : value(value) { std::cout << value ; }
~X() { std::cout << value ; }
} ;
int main()
{
X a[3] = { 1, 2, 3 } ;
}
X型の配列の要素は、a[0], a[1], a[2]の順番に構築され、a[2], a[1], a[0]の順番に破棄される。したがって、出力は、123321となる。
クラスのオブジェクトの初期化子には括弧に囲まれた式リストを使うことができる。この場合、適切な仮引数リストのコンストラクターによって初期化される。
struct X
{
X( int ) ;
X( int, int ) ;
X( int, int, int ) ;
} ;
int main()
{
X x1( 1 ) ; // X::X(int)
X x2( 1, 2 ) ; // X::X(int,int)
X x3( 1, 2, 3 ) ; // X::X(int,int,int)
}
詳しくは、初期化子の直接初期化を参照。
また、=(イコール)記号に続いて値を指定することで、初期化することもできる。
struct X
{
X( int ) ;
} ;
int main()
{
X x = 0 ; // X::X(int)
}
詳しくは、初期化子のコピー初期化を参照。
クラスのオブジェクトは、初期化リストで初期化することができる。
struct X
{
X( int ) { }
} ;
int main()
{
X a[3] = { 1, 2, 3 } ;
}
詳しくは、リスト初期化を参照。
基本クラスは、コンストラクター初期化子により初期化できる。データメンバーは、コンストラクター初期化子か、メンバーの宣言に続く初期化子によって、初期化できる。また、コンストラクターはデリゲート(Delegate)できる。
コンストラクター初期化子
クラスのコンストラクターの定義で、直接の基本クラス、virtual基本クラス、非staticデータメンバーを初期化できる。文法は、以下のようになる。
コンストラクター初期化子:
: メンバー初期化子, メンバー初期化子 ...opt
メンバー初期化子:
メンバー初期化識別子 ( 式リスト )
メンバー初期化識別子 初期化リスト
メンバー初期化識別子:
クラス名
decltype
識別子
struct Base
{
Base( int ) { }
} ;
struct Derived : Base
{
int member1 ;
int member2 ;
Derived()-
// コンストラクター初期化子
: Base( 0 ), // 基本クラス
member1( 0 ), // メンバー
member2{ 0 } // メンバー(初期化リスト)
{ }
} ;
非修飾のメンバー初期化識別子は、まずコンストラクターのクラスのスコープ内で名前探索され、見つからなかった場合は、基本クラスのスコープから探される。そのため、基本クラスの名前と、クラスの非staticデータメンバーの名前が衝突した場合、必ずメンバーの名前が使われる。その場合、基本クラスを指定するには、修飾名を用いなければならない。
struct Base { } ;
struct Derived : Base
{
int Base ;
Derived() :
Base(), // Derivedの非staticデータメンバー
Derived::Base() // 基本クラス
{ }
} ;
メンバー初期化識別子として使える名前は、直接の基本クラスと、virtual基本クラス、コンストラクターのクラスの非staticデータメンバーである。
struct A { } ;
struct B { } ;
struct C : B, virtual A { } ;
struct D : C
{
D() :
C(), // OK
A() // OK
// BはDの直接の基本クラスではないので使えない
{ }
} ;
メンバー初期化子識別子には、基本クラスの型を指し示すtypedef名やdecltype指定子を使うこともできる。
struct A { } ;
typedef A type ;
struct B { } ;
B b ;
struct C : A, B
{
C() : type(), decltype(b)()
{ }
} ;
複数の共用メンバーのうちの、ひとつだけを、メンバー初期化子で初期化することができる。
union U
{
int a ; int b ;
U() : a(0) { } // OK、ひとつだけ
} ;
struct S
{
union { int a ; int b ; } ;
S() : a(0) { } // OK ひとつだけ
} ;
union Error
{
int a ; int b ;
Error() : a(0), b(0) // エラー、複数の指定
{ }
} ;
同じunionのメンバーである共用メンバーは、オブジェクト上のストレージを共有しているので、複数初期化することはできない。
メンバー初期化子に、同じメンバー名、あるいは基本クラス名を、複数指定することはできない。
struct Base { } ;
struct Derived : Base
{
int member ;
Derived()
: member(), member(), // エラー、同じメンバー名
Base(), Base() // エラー、同じ基本クラス名
{ }
} ;
コンストラクターのデリゲートについては、コンストラクターのデリゲートを参照。
メンバー初期化が明示的に指定されておらず、アブストラクトクラスのvirtual基本クラスでもない非staticデータメンバーと基本クラスは、次のように初期化される。
非staticデータメンバーに初期化子が指定されている場合、初期化子の方法に従って初期化される。
struct S
{
int member = 123 ;
S() /*メンバー初期化子によるmemberの指定なし*/ { }
} ;
この例では、memberは、123で初期化される。
共用メンバーの場合、初期化されない。
union U
{
int member ;
U() /*メンバー初期化子による共用メンバー指定なし*/ { }
} ;
struct S
{
union { int member ; } ;
S() /*メンバー初期化子による共用メンバーの指定なし*/ { }
} ;
union initialize
{
int member ;
initialize() : member(0) { } // memberを0で初期化
} ;
共用メンバーには、明示的な初期化が必要である。
それ以外の場合、デフォルト初期化される。
struct X { X() { } } ;
struct S
{
int m1 ;
X m2 ;
S() { }
} ;
この例では、int型の非staticデータメンバーm1の初期化処理はデフォルト初期化で定義されているように、何もしない。X型m2は、X型のデフォルトコンストラクターによって初期化される。
同じunionに属する非staticな共用メンバーは、ひとつしか初期化できない。
union U
{
int m1 ; int m2 ;
U() : m1(0), m2(0) { } // エラー
} ;
struct X
{
union { int m1 ; int m2 ; } ;
X() : m1(0), m2(0) { } // エラー
} ;
struct Y
{
union { int m1 ; } ;
union { int m2 ; } ;
Y() : m1(0), m2(0) { } // OK、違うunionの共用メンバー
} ;
Yの例は、違うunionの共用メンバーなので、問題のないコードである。
クラスのコンストラクターの実行が終了した時点で、初期化も明示的な値の設定もされていないメンバーの値は、不定である。
struct X
{
int member ;
X() { }
} ;
X x1 ; // staticストレージ上に構築されたオブジェクトは、ゼロ初期化されるので、x1.memberの値は0
int main()
{
X x2 ; // x2.memberの値は不定
}
非staticデータメンバーの宣言に初期化子があり、メンバー初期化子も指定されている場合、メンバー初期化子が優先される。この場合、メンバー宣言の初期化子は無視される。
struct X
{
int member = 1;
X() { } // memberは1で初期化される
X( int arg ) : member( arg ) { } // memberはargで初期化される
} ;
int main()
{
X x1 ; // x1.memberの値は1
X x2(2) ; // x2.memberの値は2
}
デリゲートしていないコンストラクターにおける初期化は、以下のように行われる。
まず始めに、クラスが最も派生した型である場合、virtual基本クラスが初期化される。
struct V { } ;
struct B : virtual V { } ;
struct C : B { } ;
struct D : C { } ;
int main()
{
D d ; // DのコンストラクターでVが初期化される
C c ; // CのコンストラクターでVが初期化される。
B b ; // BのコンストラクターでVが初期化される。
}
virtual基本クラスは、最も派生したクラスで初期化されるということには、注意が必要である。例えば、以下のような場合、
struct V
{
int member ;
V( int arg ) : member( arg ) { }
} ;
struct B : virtual V
{
B() : V(1) { }
} ;
struct C : B { } ;
int main()
{
C c ; // エラー、Vのデフォルトコンストラクターは暗黙にdelete定義されている。
}
VはCで初期化されるので、Bによる初期化は、無視されてしまう。Cのメンバー初期化子には、Vは記述されていないので、Vはデフォルト初期化される。Vのデフォルトコンストラクターは暗黙にdelete定義されているので、エラーとなる。
複数のvirtual基本クラスを持つ場合、初期化の順番は、深度優先(depth-first)かつ、左から右(left-to-right)となる。「深度」とは、基本クラスに行くほど深くなる。「左から右」とは、基本クラス指定子リストに現れるvirtual基本クラスの順番である。
struct V1 { } ; struct V2 { } ; struct V3 { } ;
struct B : virtual V1, virtual V2 { } ;
struct C : B, virtual V3 { } ;
C c ; // V1, V2, V3の順番で初期化される
virtual基本クラスの初期化が終わった後で、直接の基本クラスが、基本クラス指定子リストに現れる順番で初期化される。メンバー初期化子は、初期化の順番に影響しない。
struct B1 { } ; struct B2 { } ;
struct D : B1, B2
{
D() : B2(), B1() { }
} ;
D d ; // B1, B2の順番に初期化される
メンバー初期化子は、初期化の順番に何の影響も与えないことに注意しなければならない。これは、副作用が初期化に影響をあたえるような場合、問題になる。
int i ;
struct B1 { B1(int) { } } ;
struct B2 { B2(int) { } } ;
struct D1 : B1, B2
{
D1() : B2(++i), B1(++i) { }
} ;
struct D2 : B2, B1
{
D2() : B2(++i), B1(++i) { }
} ;
int main()
{
i = 0 ;
D1 d1 ; // B1(1)、B2(2)で初期化される
D2 d2 ; // B2(1)、B1(2)で初期化される
}
メンバー初期化子の順番は、基本クラスの初期化順序に影響しない。そのため、ある基本クラスの初期化における副作用が、次の基本クラスの初期化に影響をあたえるようなコードでは、基本クラスの記述の順番を変えるだけで、初期化の結果が異なってしまう。一般に、直接の基本クラスの初期化の順番が保証されていることを前提にしたコードを書くべきではない。
直接の基本クラスの初期化が終わった後で、クラス定義内の非staticデータメンバーが、宣言されている順番で初期化される。メンバー初期化子は、初期化の順番に影響しない。
struct X
{
int m1 ;
int m2 ;
X() : m2(0), m1(0) { }
} ;
X x ; // m1, m2の順番で初期化される
直接の基本クラスの場合と同じく、メンバー初期化子は初期化の順番に影響しないということに注意しなければならない。非staticデータメンバーの初期化の順番は、クラス定義の中でメンバーが宣言されている順番である。したがって、あるメンバーの初期化の副作用が、次のメンバーの初期化に影響をあたえるようなコードでは、メンバーの宣言の順番を変えただけで、初期化処理が異なってしまう。具体的な問題例は、直接の基本クラスの場合と同じである。一般に、非staticデータメンバーの初期化の順番が保証されていることを前提にしたコードを書くべきではない。
最後に、コンストラクターの本体が実行される。
struct V { } ;
struct B { } ;
struct M { } ;
struct D : B, virtual V
{
M m ;
D() { /* コンストラクターの本体*/ }
} ;
D d ; // V, B, m, コンストラクターの本体の順番で初期化される
メンバー初期化子における名前は、コンストラクターの本体で評価される。
int ;
struct X
{
int x ;
int y ;
X() : x(0), y(x) { }
} ;
メンバー初期化子では、thisを使うことができる。ただし、thisの参照先はまだ構築途中である場合もあるので、注意が必要である。
非staticメンバー関数は、virtual関数を含めて、構築中のオブジェクトであっても呼び出すことができる。また、構築途中のオブジェクトを、typeid演算子やDynamic cast(Dynamic cast)のオペランドに渡すこともできる。
ただし、コンストラクター初期化子において、まだすべての基本クラスの初期化が終わっていない時点で、この種の操作を行った場合、結果は未定義である。これは、間接的に操作が行われる場合も含む。
struct A { A(int) { } } ;
struct B : A
{
int f() { return 0 ; }
B() : A( f() ) // 結果は未定義
{ }
} ;
// 間接的に操作が行われる例
struct C : A
{
static int call_f( C * ptr ) { return ptr->f() ; }
int f() { return 0 ; }
C() : A( call_f( this ) ) // 未定義
{ }
} ;
構築中のオブジェクトに対してvirtual関数を呼び出したり、typeidやdynamic_castを使った場合の挙動は、生成と破棄を参照。
メンバー初期化子では、パック展開できる。
template < typename Bases >
struct X : Bases...
{
X() : Bases()...
{ }
} ;
メンバー初期化識別子に、クラス型を指定することによって、別のコンストラクターに初期化処理を委譲することができる。これを、コンストラクターのデリゲート(delegate)という。
struct X
{
int member ;
X( int value ) : member( value )
{ /* 初期化処理 */ }
X( double d ) : X(123) // コンストラクターのデリゲート
{
// 追加の処理
}
} ;
この例では、コンストラクターX::X(double)は、初期化処理をX::X(int)にデリゲートしている。
別のコンストラクターにデリゲートしているコンストラクターのことを、デリゲートコンストラクター(delegating constructor)といい、デリゲート先のコンストラクターのことを、ターゲットコンストラクター(target constructor)という。またオブジェクトの初期化のために最初に呼び出されたコンストラクターのことを、最初のコンストラクター(principal constructor)という。
struct X
{
X() : X( 0 ) { }
X( int ) : X( 0.0 ) { }
X( double ) { }
} ;
X x ; // 初期化
上に例における、Xのオブジェクトxの初期化では、最初のコンストラクターとして、X::X()が選ばれる。これは、デリゲートコンストラクターであり、ターゲットコンストラクターであるX::X(int)にデリゲートする。X::X(int)もデリゲートコンストラクターであり、ターゲットコンストラクターのX::X(double)にデリゲートする。
デリゲートコンストラクターは、他のメンバー初期化識別子を指定してはならない。
struct Base { } ;
struct X : Base
{
int member ;
X() : X( 0 ),
Base(), member(0) // エラー、デリゲートコンストラクターは他の識別子を指定できない
{ }
X( int ) { }
} ;
ターゲットコンストラクターは、オーバーロード解決により選ばれる。
struct X
{
X() : X( 0 ) { } // X::X(int)を呼ぶ
X( int ) : X( 0.0 ) { } // X::X(double)を呼ぶ
X( double ) { }
} ;
ターゲットコンストラクターが処理を返した後に、デリゲートコンストラクターの本体が実行される。
struct X
{
int member ;
X() : X( 0 )
{ /* 処理2 */ }
X( int value ) : member( value )
{ /* 処理1 */ }
} ;
X x ;
この例では、オブジェクトxの初期化の際、最初のコンストラクターとしてX::X()が選ばれる。これはデリゲートコンストラクターである。ターゲットコンストラクターは、X::X(int)となる。ターゲットコンストラクターは、通常のコンストラクターと同じように基本クラスやメンバーの初期化を終えた後、コンストラクターの本体を実行し(処理1)、処理を返す。ターゲットコンストラクターが処理を返したので、最初のコンストラクターの本体が実行される(処理2)。
デリゲートコンストラクターが、直接的にせよ、間接的にせよ、自分自身にデリゲートを行った場合は、エラーとなる。
struct X
{
X() : X() {} // エラー、直接的な自分自身へのデリゲート
} ;
struct Y
{
Y() : Y(0) { } // エラー、間接的な自分自身へのデリゲート
Y(int) : Y(0.0) { } // エラー、間接的な自分自身へのデリゲート
Y(double) : Y() { } // エラー、間接的な自分自身へのデリゲート
} ;
クラスYは、間接的に、自分自身へのデリゲートを行う例である。デリゲートのネスト、つまり他のデリゲートコンストラクターへのデリゲートは可能である。ただし、間接的であっても、自分自身へのデリゲートは認められない。
クラスのオブジェクトが構築される前、破棄された後、あるいは構築や破棄の最中には、いくつか気を付けなければならないことがある。
非トリビアルコンストラクターを持つクラスのオブジェクトのコンストラクターの実行が始まる前に、非staticメンバーや基本クラスにアクセスした場合、挙動は未定義である。
struct X
{
X() { } // 非トリビアルコンストラクター
int member ;
} ;
int main()
{
X * ptr = static_cast<X *>( operator new( sizeof(X) ) ) ; // 初期化されていないストレージ
ptr->member ; // 未定義
&ptr->member ; // 未定義、ポインターを得ることもできない
new(ptr) X ; // 初期化
ptr->member ; // OK
&ptr->member ; // OK
operator delete( ptr ) ;
}
非トリビアルデストラクターを持つクラスのオブジェクトのデストラクターの実行が終わった後に、非staticメンバーや基本クラスにアクセスした場合、挙動は未定義である。
struct X
{
~X() { } // 非トリビアルデストラクター
int member ;
} ;
int main()
{
X * ptr = static_cast<X *>( operator new( sizeof(X) ) ) ; // 初期化されていないストレージ
new(ptr) X ; // 初期化
ptr->~X() ; // デストラクターの実行
ptr->member ; // 未定義
operator delete( ptr ) ;
}
クラスへのポインターを、基本クラスへのポインターに型変換する際には、クラスとそのすべての基本クラスのコンストラクターの実行が始まっていなければならない。また、デストラクターの実行が完了していてはならない。そうでない場合の挙動は未定義である。これは、トリビアルクラスにも当てはまる。
struct X { } ;
struct Y : X { } ;
int main()
{
Y * y_ptr = static_cast<Y *>( operator new( sizeof(Y) ) ) ; // 初期化されていないストレージ
X * x_ptr = y_ptr ; // 未定義
new (y_ptr) Y ; // 初期化
x_ptr = y_ptr ; // OK
y_ptr->~Y() ; // デストラクターの実行
x_ptr = y_ptr ; // 未定義
operator delete( y_ptr ) ;
}
オブジェクトの構築や破棄の途中で、メンバー関数を呼び出すことはできる。ただし、virtual関数をコンストラクターやデストラクターの中で呼び出す際には、注意が必要である。virtual関数をコンストラクターやデストラクターの中、あるいはその中から呼び出された関数内で呼び出すと、そのコンストラクターあるいはデストラクターのクラスの型にとってのファイナルオーバーライダーが用いられ、派生クラスは考慮されない。これは、基本クラスのコンストラクターの実行中には、派生クラスはまだ完全に初期化されていないからである。
struct A
{
virtual void f() { }
virtual void g() { }
// A::f、A::gを呼び出す
A() { f() ; g() ; }
virtual ~A() { f() ; g() ; }
} ;
struct B : A
{
virtual void f() { }
// B::f, A::gを呼び出す
B() { f() ; g() ; }
virtual ~B() { f() ; g() ; }
} ;
struct C : B
{
virtual void g() { }
// B::f, C::gを呼び出す
C() { f() ; g() ; }
virtual ~C() { f() ; g() ; }
} ;
この例で、たとえクラスCのオブジェクトを構築したとしても、基本クラスAのコンストラクターの中ではA::f, A::gが呼ばれることになる。
オブジェクトの構築や破棄の途中で、typeid演算子を使うことはできる。typeid演算子がコンストラクターやデストラクターの中、あるいはその中から呼び出された関数内で使われていて、typeid演算子のオペランドが、そのクラスの構築中のオブジェクトである場合、typeidはコンストラクターやデストラクターのクラス型情報を表すstd::type_infoオブジェクトを返す。これは、基本クラスの構築中は、まだ派生クラスは構築し終わっていないからである。
struct A
{
A()
{
typeid( *this ) == typeid( A ) ; // true
}
virtual ~A()
{
typeid( *this ) == typeid( A ) ; // true
}
} ;
struct B : A { } ;
この例では、たとえBのオブジェクトが構築されたとしても、typeidはA型を表すstd::type_infoオブジェクトを返す。
オブジェクトの構築や破棄の途中で、dynamic_castを使うことはできる。dynamic_castがコンストラクターやデストラクターの中、あるいはその中から呼び出された関数内で使われていて、オペランドがそのクラスの構築中のオブジェクトである場合、コンストラクターやデストラクターの属するクラスが、最終的に派生された型であるとみなされる。これは、基本クラスの構築中は、まだ派生クラスは構築し終わっていないからである。
struct A
{
A() ;
virtual ~A() ;
} ;
struct B : A { } ;
A::A()
{
B * ptr = dynamic_cast<B *>( this ) ; // 常にnullポインター
}
A::~A()
{
B * ptr = dynamic_cast<B *>( this ) ; // 常にnullポインター
}
たとえ、Aから派生されたB型のオブジェクトであっても、Aのコンストラクター、デストラクターの中では、A型が最終的な派生クラスであるとみなされる。
クラスのオブジェクトは、初期化と代入によって、コピーもしくはムーブされる。コピーやムーブを行うためのコンストラクターと代入演算子を、それぞれ特別に、コピーコンストラクター、ムーブコンストラクター、コピー代入演算子、ムーブ代入演算子と呼ぶ。
コピーコンストラクター(copy constructor)とは、あるクラスXにおいて、非テンプレートなコンストラクターで、一つ目の仮引数の型が、X &、const X &、volatile X &、const volatile X &のいずれかであり、二つ目以降の仮引数は存在しないか、すべてデフォルト実引数があるものをいう。
struct X
{
// コピーコンストラクター
X( X & ) ;
X( X const & ) ;
X( X volatile & ) ;
X( X const volatile & ) ;
X( X const &, int x = 0, int y = 0 ) ; // 二つ目以降の仮引数にデフォルト実引数がある
// コピーコンストラクターではない
X( ) ;
X( int ) ;
template < typename T >
X( T ) ; // テンプレートコンストラクターはコピーコンストラクターではない
X( X const &, short ) ; // 二つ目以降の仮引数にデフォルト実引数がない
} ;
ムーブコンストラクター(move constructor)とは、あるクラスXにおいて、非テンプレートなコンストラクターで、一つ目の仮引数の型が、X &&、const X &&、volatile X &&、const volatile X &&のいずれかであり、二つ目以降の仮引数は存在しないか、すべてデフォルト実引数があるものをいう。
struct X
{
X( X && ) ;
X( X const && ) ;
X( X volatile && ) ;
X( X const volatile && ) ;
X( X const &&, int x = 0 ) ;
} ;
クラスXのコンストラクターの一つ目の仮引数の型がXで、二つ目以降の仮引数が存在しないか、すべてデフォルト実引数が指定されている場合は、エラーとなる。
struct X
{
X( X ) ; // エラー
} ;
また、テンプレートコンストラクターのインスタンス化の結果が、このようなシグネチャになる場合、そのテンプレートはインスタンス化されない。
struct X
{
template < typename T >
X( T ) { } // X<X>というインスタンス化は起こらない。
} ;
int main()
{
X a( 0 ) ; // テンプレートコンストラクターが使われる。
X b( a ) ; // 暗黙のコピーコンストラクターが使われる
}
あるクラスにおいて、コピーコンストラクターが明示的に宣言されていない場合、コピーコンストラクターは暗黙的に宣言される。もし、クラスにユーザー定義のムーブコンストラクター、ムーブ代入演算子、コピー代入演算子、デストラクターが存在する場合、コピーコンストラクターは暗黙的にdelete定義される。そうでない場合は、default定義される。
struct A
{
// コピーコンストラクターは暗黙的にdefault定義される
// A( A const & ) = default ;
} ;
struct B
{
B( B && ) ; // ユーザー定義ムーブコンストラクター
B & operator = ( B && ) ; // ユーザー定義ムーブ代入演算子
B & operator = ( B & ) ; // ユーザー定義コピー代入演算子
~B() ; // ユーザー定義デストラクター
// コピーコンストラクターは暗黙的にdelete定義される
// B( B const & ) = delete ;
} ;
C++98/03では、暗黙のコピーコンストラクターはユーザー定義のコピー代入演算子、ユーザー定義のムーブ代入演算子、ユーザー定義デストラクターがある場合でも、暗黙的に宣言された。C++11では、この挙動は非推奨になった。将来的に取り除かれる予定だ。
struct S
{
~S() { }
} ;
int main()
{
S s1 ;
// OK: C++98, C++03まで
// 非推奨: C++11以降
S s2( s1 ) ;
}
理由は、そのようなユーザー定義の特別なメンバー関数がある場合は、大抵、暗黙のデフォルトのコピーコンストラクターの挙動は、容易にプログラミング上の誤りを引き起こすためである。
ユーザー定義のコピー代入演算子、ユーザー定義のムーブ代入演算子、ユーザー定義デストラクターがある場合の暗黙のコピーコンストラクターの宣言は、非推奨であり、将来の規格では取り除かれる。そのため、このような非推奨に依存したコードを書いてはならない。
クラスXの暗黙のコピーコンストラクターのシグネチャは、通常、
X::X( const X & )
となる。ただし、直接の基本クラスやvirtual基本クラス、非staticデータメンバーがconst修飾されていない仮引数のコンストラクターを持つ場合、
X::X( X & )
となる。
struct A
{
A() = default ;
A( A const & ) { }
} ;
struct B : A
{
// 暗黙のコピーコンストラクターのシグネチャ
// B( B const & ) = default ;
} ;
struct C
{
C() = default ;
C( C & ) { }
} ;
struct D : C
{
// 暗黙のコピーコンストラクターのシグネチャ
// D( D & ) = default ;
} ;
あるクラスにおいて、ムーブコンストラクターが明示的に宣言されていない場合、ムーブコンストラクターは暗黙的に宣言される。もし、あるクラスがユーザー定義の、コピーコンストラクター、コピー代入演算子、ムーブ代入演算子、デストラクターを持たず、またムーブコンストラクターがdelete定義されていない場合、ムーブコンストラクターはdefault定義される。
ユーザー定義のムーブ代入演算子が存在する場合、ムーブコンストラクターは暗黙的にdefault定義されない。これは、デフォルトのムーブコンストラクターの挙動と、ユーザー定義のムーブ代入演算子の挙動が異なる可能性があるため、安全のためにdefault定義されないのである。
struct X
{
// ムーブコンストラクターはdefault定義されない
// ユーザー定義のムーブ代入演算子
X & operator = ( X && obj )
{
// ユーザー定義のムーブを実装
}
} ;
そのため、自前実装のムーブ構築とムーブ代入を行いたい場合、ムーブコンストラクターとムーブ代入演算子を両方ユーザー定義する必要がある。
ムーブコンストラクターが、暗黙にも明示的にも宣言されていない場合、ムーブコンストラクターを呼び出す式は、代わりにコピーコンストラクターを呼び出す。
struct X
{
X() = default ;
// ユーザー定義のコピーコンストラクター
X( X const & ) { }
// ムーブコンストラクターは宣言されない
} ;
int main()
{
X a ;
// コピーコンストラクターを呼び出す
X b( static_cast< X && >( a ) ) ;
}
クラスXの暗黙のムーブコンストラクターのシグネチャは、以下の通りである。
X::X( X && )
あるクラスが以下の条件を満たした時、対応するコピー/ムーブコンストラクターはdelete定義される。つまり、以下の条件でコピーができない場合はコピーコンストラクターが、ムーブができないときはムーブコンストラクターが、それぞれ個別にdelete定義される。
-
クラスがunionのようなクラスで、共用メンバーがそれぞれ非トリビアルなコピー/ムーブ・コンストラクターを持つ場合
struct NonTrivial
{
NonTrivial( NonTrivial const & ) { }
NonTrivial( NonTrivial && ) { }
} ;
struct X
{
X() { }
union { NonTrivial n ; } ;
// コピーコンストラクターは、nが非トリビアルなコピーコンストラクターを持つためにdelete定義される
// ムーブコンストラクターは、nが非トリビアルなムーブコンストラクターを持つためにdelete定義される。
} ;
-
非staticデータメンバーが、オーバーロード解決の結果、コピー/ムーブできない場合
オーバーロード解決の結果というのは、複数の候補があって曖昧である場合、選ばれた最適関数がdeleted定義されている場合、アクセス指定によりクラスのデフォルトコンストラクターからは利用できない場合だ。
// コピーできない
struct uncopyable
{
uncopyable( uncopyable const & ) = delete ;
} ;
// コピーコンストラクターがdelete定義される
struct S
{
uncopyable member ;
} ;
アクセス指定によりクラスのデフォルトコンストラクターからは利用できない場合の例
struct private_copy
{
private :
private_copy( private_copy const & ) = delete ;
} ;
// コピーコンストラクターがdelete定義される
struct S
{
// memberのコピーコンストラクターは
// クラスSのデフォルトコンストラクターからアクセスできない
private_copy member ;
} ;
-
直接、あるいはvirtualな基本クラスが、オーバーロード解決の結果、コピー/ムーブできない場合
-
直接、あるいはvirtualな基本クラス、もしくは非staticデータメンバーの、デストラクターが、delete定義であるか、あるいはアクセス指定により、デフォルトコンストラクターからアクセスできない場合
// デストラクターがprivateメンバーのクラス
struct M
{
private :
~M() { }
} ;
// コピーとムーブコンストラクターがdelete定義される
struct S
{
M m ;
} ;
-
非staticデータメンバーがrvalueリファレンス型である場合、コピーコンストラクターがdelete定義される。
// コピーコンストラクターがdelete定義される
struct S
{
static int data ;
// rvalueリファレンス型の非staticデータメンバー
int && rref ;
S() : rref( static_cast< int && >(data) ) { }
} ;
int S::data ;
int main()
{
S s1 ;
S s2 = s1 ; // エラー、コピーコンストラクターがdelete定義されている
}
デフォルト定義されたムーブコンストラクターがdeleted定義されている場合、オーバーロード解決では無視される。
コピー/ムーブコンストラクターがトリビアル(trivial)となるには、ユーザー定義されず、仮引数リストが暗黙に宣言された場合の仮引数リストと同じで、以下の条件をすべて満たす必要がある。
-
クラスはvirtual関数とvirtual基本クラスを持たないこと
-
volatile修飾された非staticデータメンバーを持たないこと
-
直接の基本クラスのサブオブジェクトをコピー/ムーブするのに使われるコンストラクターがトリビアルであること
-
クラス型か、クラスの配列型の非staticデータメンバーをコピー/ムーブするのに使われるコンストラクターがトリビアルであること
これらの条件をすべて満たさない限り、コピー/ムーブコンストラクターは、非トリビアル(non-trivial)である。
コピー/ムーブコンストラクターが、default化されていて、定義もdeleted定義もされていない場合、ODRの文脈で使われるか、最初の宣言で明示的にdefault化された場合、暗黙に定義される(implicitly defined)。もし、暗黙に定義されたコンストラクターがconstexprコンストラクターの制約を満たすのならば、暗黙に定義されたコンストラクターは、constexprコンストラクターになる。
コピー/ムーブコンストラクターが暗黙に定義されるためには、直接の基本クラス、virtual基本クラス、非staticデータメンバーの、ユーザー提供されていないコピー/ムーブコンストラクターは、すべて暗黙に定義されていなければならない。ユーザー提供されている場合は、暗黙に定義されていなくてもよい。
struct Base
{
// コピーコンストラクターは暗黙に定義されていない
Base( const Base & ) = delete ;
} ;
struct Derived : Base
{
// コピーコンストラクターは暗黙にdefault化される
} ;
void f()
{
Derived d1 ;
Derived d2 = d1 ; // エラー、コピーコンストラクターは暗黙に定義されていない
}
unionではないクラスの暗黙に定義されたコピー/ムーブコンストラクターは、、基本クラスとメンバーに対し、メンバーごとのコピー/ムーブを行う。初期化の順番は、ユーザー定義されるコンストラクターの場合と同じである。
union型の暗黙に定義されたコピー/ムーブコンストラクターは、union型のオブジェクトの内部表現をコピーする。
ユーザー宣言されたクラスXのコピー/ムーブ代入演算子、X::operator =は、クラスXの非static、非テンプレートのメンバー関数で、仮引数を一つだけ取り、その型はX, X &, const X &, volatile X & const volatile X &でなければならない。
struct X
{
X & operator = ( X & ) ;
X & operator = ( X const & ) ;
X & operator = ( X volatile & ) ;
X & operator = ( X const volatile & ) ;
} ;
ユーザー宣言されるコピー/ムーブ代入演算子の戻り値の型は制限されていない。
struct X
{
// OK
void operator = ( X const & ) { }
} ;
void f()
{
X x1 ;
X x2 ;
x1 = x2 ; // OK、結果の型はvoid
}
コンストラクターと同じく、代入演算子でも、代入演算子のテンプレートは、コピー/ムーブ代入演算子ではない。ただし、コンストラクターと同じように、テンプレートの特殊化がコピー/ムーブ代入演算子と同じ仮引数になった場合は、オーバーロード解決の候補となり、非テンプレートのコピー/ムーブ代入演算子より優先して選択されることもある。このため、代入演算子のテンプレートは、コピー風/ムーブ風の代入演算子と非公式に呼ばれている。
struct X
{
X() { }
X & operator = ( X const & ) ; // #1
template < typename T >
X & operator = ( T & ) ; // #2
} ;
void f()
{
X x1 ;
X x2 ;
x1 = x2 ; // #2が呼ばれる
}
もし、クラス定義の中でコピー代入演算子が明示的に宣言されていない場合、コピー代入演算子は暗黙的に宣言される。
struct X
{
// コピー代入演算子が暗黙的に宣言される
} ;
もし、クラス定義で、ムーブコンストラクター、ムーブ代入演算子、コピーコンストラクター、デストラクターがユーザー宣言されていた場合、暗黙に宣言されたコピー代入演算子は、delete定義される。
struct A
{
// コピー代入演算子はdelete定義される
A( A && ) ;
} ;
struct B
{
// コピー代入演算子はdelete定義される
B & operator = ( B && ) ;
} ;
C++11では、コピーコンストラクター、デストラクターがユーザー宣言されていた場合、コピー代入演算子はdefault化される。この挙動は非推奨であり、将来的には取り除かれる。このような非推奨の機能に頼ったコードを書いてはならない。
// このようなコードを書いてはならない
struct X
{
// C++11では、コピー代入演算子は暗黙にdefault化される
// この機能は非推奨であり、使ってはならない
X( X const & ) ;
~X() ;
} ;
暗黙に宣言される、クラスXのコピー代入演算子は、以下の条件、
-
クラスXの直接の基本クラスBが、仮引数の型としてconst B &, const volatile B &, Bのいずれかであるコピー代入演算子を持つ
struct B1 { B1 & operator = ( const B & ) ; } ;
struct B2 { B2 & operator = ( const volatile B & ) ; } ;
struct B3 { B3 & operator = ( B ) ; } ;
struct X : B1, B2, B3 { } ;
-
Xの非staticデータメンバー、もしくは配列がすべて、型をMとおくと、仮引数の型がconst M &, const volatile M &, Mであるコピー代入演算子を持つ
をすべて満たす場合、
X & X::operator = ( const X & )
の形を取る。
上の条件を満たさない場合、暗黙に宣言されるクラスXのコピー代入演算子の形は、
X & X::operator = ( X & )
となる。
クラスXのユーザー宣言されるムーブ代入演算子、X::operator =は、非static、非テンプレートのメンバー関数で、仮引数を一つだけ取り、その型は、X &&, const X &&, volatile X &&, const volatile X &&でなければならない。
あるクラスXの定義でムーブ代入演算子が明示的に宣言されていない場合は、以下の条件をすべて満たした時、暗黙的にムーブ代入演算子が宣言される。
-
クラスXはユーザー宣言されたコピーコンストラクターを持たない
-
クラスXはユーザー宣言されたムーブコンストラクターを持たない
-
クラスXはユーザー宣言されたコピー代入演算子を持たない
-
クラスXはユーザー宣言されたデストラクターを持たない
-
ムーブ代入演算子は暗黙にdelete定義されていない
暗黙に宣言されたクラスXのムーブ代入演算子は、以下の形を取る。
X & X::operator = ( X && )
暗黙に宣言されたクラスXのコピー/ムーブ代入演算子は、X &型の戻り値を返す。戻り値として返されるのは代入演算子が呼ばれたクラスのオブジェクトへのリファレンスである。暗黙に宣言されたコピー/ムーブ代入演算子は、クラスのinline publicメンバーとなる。
デフォルト化されたクラスXのコピー/ムーブ代入演算子は、クラスXが以下のいずれかのメンバーを持つ時、それぞれ対応するコピー/ムーブ代入演算子が、delete定義される。
-
クラスXがunion風クラスで、その共用メンバーに、非トリビアルなコピー/ムーブ代入演算子があるとき
union風クラスとは、unionか無名unionを含むクラスである。
// 非トリビアルなコピー代入演算子を持つ型
struct S
{
S & operator = ( S const & ) { }
} ;
union U1 { S s ; } ;
struct U2
{
union { S s ; } ;
} ;
int main()
{
U1 a ;
U1 b ;
a = b ; // エラー、コピー代入演算子がdelete定義されている
U2 c ;
U2 d ;
c = d ; // エラー、コピー代入演算子がdelete定義されている
}
-
const修飾された非クラスの、型か配列型を、非staticデータメンバーとして持つ場合
struct S
{
const int member ;
const int array[10] ;
} ;
-
リファレンス型の非staticデータメンバーを持つ場合
-
オーバーロード解決の結果、コピー/ムーブ代入できないクラス型を非staticデータメンバーとして持つ場合。
コピー/ムーブ代入できないというのは、オーバーロード解決の結果が曖昧であるか、最適候補がdeleted定義であるか、アクセス指定により、クラスXのデフォルト代入演算子からアクセスできない場合をいう。以下同じ
-
直接、あるいはvirtualな基本クラスが、オーバーロード解決の結果、コピー/ムーブ代入できない場合。
デフォルト化されたムーブ代入演算子がdelete定義された場合、オーバーロード解決では無視される。
コピー/ムーブ代入演算子は、明示的に宣言されなかった場合、必ず暗黙的に宣言される。宣言の方法は様々で、delete定義になることもあるが、宣言されることはされる。これは、クラスの基本クラスの代入演算子は、必ず隠されるということだ。using宣言を使って基本クラスの代入演算子をクラススコープに導入しても、そのクラスで宣言された代入演算子が優先されるため、やはり隠される。
あるクラスXのコピー/ムーブ代入演算子がトリビアルであるためには、ユーザー提供されず、仮引数リストが、暗黙に宣言された場合の仮引数リストと同じで、さらに以下の条件をすべて満たさなければならない。
-
クラスXはvirtual関数を持たず、virtual基本クラスも持たない
-
クラスXはvolatile修飾された型の非staticデータメンバーを持たない
-
直接の基本クラスのサブオブジェクトで使われるコピー/ムーブの代入演算子がトリビアル
-
クラスXのクラス型とクラスの配列型の非staticデータメンバーの、コピー/ムーブに使われる代入演算子がトリビアル
この条件を満たさない場合、コピー/ムーブ代入演算子は非トリビアルとなる。
あるクラスXの、default化されているが、delete定義されていないコピー/ムーブコンストラクターは、ODRの文脈で使われた場合、もしくは、最初の宣言で明示的にdefault化された場合、暗黙に定義される。
暗黙に定義されたコピー/ムーブ代入演算子は、以下の条件をすべて満たした場合、constexpr関数になる。
-
クラスXはリテラル型
-
直接の基本クラスのサブオブジェクトで、コピー/ムーブのために使われる代入演算子が、constexpr関数
-
クラスXの、クラス型、もしくはクラスの配列型の非staticデータメンバーの、コピー/ムーブのために使われる代入演算子が、constexpr関数
あるクラスのデフォルト化されたコピー/ムーブ代入演算子が暗黙に定義されるには、クラスの直接の基本クラスと、非staticデータメンバーのコピー/ムーブ代入演算子のうち、ユーザー提供されていないものは、すべて暗黙に定義されていなければならない。
union以外のクラスで、暗黙に定義されたコピー/ムーブ代入演算子は、クラスのサブオブジェクトに対してメンバーごとのコピー/ムーブを行う。クラスの直接の基本クラスがまず代入される。代入の順序は、基本クラス指定のリストで宣言された順番である。
struct A { } ;
struct B { } ;
// 代入されるときは、A, Bの順番
struct C : A, B { } ;
// 代入されるときは、B, Aの順番
struct D : B, A { } ;
その次に、クラスの非staticデータメンバーが、クラス定義で宣言された順番で代入される。サブオブジェクトがクラス型の場合はoperator =を呼び出し、基本型の場合は組み込みの代入演算子を使い。配列型の場合は、要素ごとに代入される。
暗黙に定義されたコピー代入演算子が、複数回派生されているvirtual基本クラスのサブオブジェクトのコピー代入演算子を、派生された回数だけ呼び出すかどうかは、未規定である。
struct V
{
V & operator = ( V const & ) ; // #1
} ;
struct A : virtual V { } ;
struct B : virtual V { } ;
struct C : A, B { } ; // 暗黙のコピー代入演算子がdefault定義される
int main()
{
C c1 ;
C c2 ;
c2 = c1 ; // #1が一度呼ばれるか、二度呼ばれるかは、未規定
}
クラスCには、VはA,B二つのクラスのvirtual基本クラスになっているため、クラスCには、Vのサブオブジェクトはひとつしかない。この場合、Vのコピー代入演算子が、一回呼ばれるとは限らない。ただし二回呼ばれるとも限らない。規格上、この場合に、暗黙に定義されたコピー代入演算子は、Vのコピー代入演算子を呼び出す回数は、一回でも二回でも良い。
なお、ムーブ代入演算子は、virtual基本クラスがある場合、暗黙にdelete定義されるので、この未規定はない。
もちろん、暗黙の定義ではなく、明示的に定義した場合は、明示的に書いただけの回数呼び出す。
unionの暗黙に定義されたコピー代入演算子は、オブジェクトの内部表現をコピーする。
コピー/ムーブのコンストラクターか代入演算子がODRの文脈で使われていて、アクセス指定のためにアクセスできない場合、エラーとなる。
条件次第で、C++の実装はオブジェクトのコピー/ムーブ構築を省略することができる。たとえ、そのオブジェクトのコンストラクターやデストラクターが、副作用を持っていたとしても、遠慮なく省略される。これをコピー省略(copy elision)という。規格上の用語は「コピー省略」だが、コピーだけではなく、ムーブも省略される可能性がある。
コピー省略は、以下の場合に許されている。
-
クラスを返す関数のreturn文のオペランドの式が、関数とcatch句の仮引数を除く非volatileの自動オブジェクトの名前であり、関数の戻り値の型と自動オブジェクトの型が、ともにCV非修飾の型である場合、関数内の自動オブジェクトのコピー/ムーブが省略され、関数の戻り値のオブジェクトとして直接に構築することが許されている
struct S
{
S() { }
S( S const & ) { }
~S() { }
} ;
S f()
{
S s ;
return s ; // コピー省略が許されている
}
-
throw式のオペランドが、関数とcatch句の仮引数を除く非volatileの自動オブジェクトの名前であり、その自動オブジェクトのスコープが、最も内側のブロックの外に出ない場合、例外オブジェクトに対するコピー/ムーブが省略され、例外オブジェクト上に直接構築することが許されている。
struct S
{
S() { }
S( S const & ) { }
~S() { }
} ;
int main()
{
try
{
S s ; // 最も内側のブロックスコープの外に出ない
throw s ; // コピー省略が許されている
} catch ( S & s ) { }
}
-
リファレンスで束縛されていないクラスの一時オブジェクトが、同じCV非修飾の型に、コピー/ムーブされた場合、コピー/ムーブは省略され、コピー/ムーブ先のオブジェクトに、一時オブジェクトを直接構築することが許されている。
struct S
{
S() { }
S( S const & ) { }
~S() { }
} ;
S f()
{
return S() ; // コピー省略が許されている
}
int main()
{
S s = f() ; // コピー省略が許されている
}
-
例外ハンドラーの例外宣言が、CV修飾子以外は同じ型のオブジェクトを、例外オブジェクトとして宣言している場合、コンストラクターとデストラクター呼び出し以外にプログラムの意味が変わらなければ、コピー/ムーブは省略され、例外宣言の仮引数は、例外オブジェクトを参照することが許されている。
struct S
{
S() { }
S( S const & ) { }
~S() { }
} ;
int main()
{
try
{
throw S() ;
}
// コピー省略が許されている
catch( S s ) { }
}
「コンストラクターとデストラクター呼び出し以外にプログラムの意味が変わらない」というのは、たとえば例外オブジェクトを変更するような場合だ。
catch( S s )
{
s = S() ; // 例外オブジェクトを変更
}
例外宣言のリファレンスではない仮引数の指し示すオブジェクトに変更を加えても、元の例外オブジェクトを変更しないと規定されているので、この場合は、コピー省略はできない。
コピー省略の条件は、組み合わさった場合でも、コピー省略されることが許されている。
コピー省略できる条件がそろい、コピーされるオブジェクトがlvalueの場合、オーバーロード解決が二回行われることがある。一回目のオーバーロード解決は、オブジェクトをrvalueとして、コピーするコンストラクターを探す。一回目のオーバーロード解決が失敗するか、選択されたコンストラクターの第一引数がrvalueリファレンスではない場合、オブジェクトをlvalueとして、二回目のオーバーロード解決が行われる。
一回目のオーバーロード解決で、選択されたコンストラクターの第一引数がrvalueリファレンスではない場合というのは、たとえばconst修飾されたlvalueリファレンスが該当する。
この二回のオーバーロード解決は、たとえコピー省略をしないとしても、必ず行われる。この規定の目的は、コピー省略が行われなかったならば呼び出されるコンストラクターの、アクセス指定を調べるためである。
using宣言を使って派生クラスから基本クラスのコンストラクターを指定することで、基本クラスのコンストラクターを明示的に継承できる。これにより、機械的な手書きのコードを省くことができる。
class Base
{
private :
int member ;
public :
Base( int value ) : member(value) { }
} ;
class Derived : Base
{
public :
// Base::Base(int)を継承
using Base::Base ;
} ;
int main()
{
Derived d(0) ; // 継承コンストラクターを使う
}
using宣言は通常通り、アクセス指定の影響を受けることに注意すること。派生クラスによって継承された基本クラスのコンストラクターは、同じ仮引数をとり、引数をそのままメンバー初期化子で基本クラスに渡し、関数本体は空であるコードを手書きした場合と同じように動く。
struct Base { Base(int, double) { } } ;
struct Derived : Base
{
// Baseクラスのコンストラクターの継承
using Base::Base ;
// 以下のコードを手書きした場合と同等
// Derived( int p1, double p2 )
// : Base( p1, p2 )
// { }
} ;
このような手書きのコンストラクターを、実際に使うとエラーとなる場合、継承コンストラクターの使用もエラーとなる。
派生クラスで同じシグネチャーのコンストラクターをユーザー定義した場合、そのコンストラクターの継承は起こらない。
struct Base
{
Base( int ) { }
Base( double ) { }
} ;
struct Derived : Base
{
using Base::Base ;
Derived( int value ) : Base(value)
{
// 処理
}
}
この場合、Base::Base(double)は継承されるが、Base::Base(int)は継承されない。クラスDerivedでユーザー定義されたコンストラクターが使用される。
継承コンストラクターの詳細はすこし難しい。まず、継承される基本クラスのコンストラクターが、継承コンストラクターの候補(candidate set of inherited constructors)として列挙される。この際、コンストラクターにデフォルト実引数がある場合は、デフォルト実引数を省略したシグネチャーの関数も追加される。たとえば、以下のようなクラスの場合、
struct A
{
A( int i ) { }
} ;
struct B
{
B( int p1 = 1, int p2 = 2 ) { }
} ;
クラスAの継承コンストラクターの候補は、以下の通り。
A( int )
A( A const & )
A( A && )
クラスBの継承コンストラクターの候補は、以下の通り。
B( ) // デフォルト実引数の省略形
B( int = 1 ) // デフォルト実引数の省略形
B( int = 1, int = 2 )
B( B const & )
B( B && )
さて、この継承コンストラクターの候補から、引数を取らないコンストラクター(デフォルトコンストラクター)、引数をひとつだけ取るコピー/ムーブコンストラクターを除くコンストラクターが継承コンストラクターとなる。これらのコンストラクターは、派生クラス側で暗黙に宣言されるものだからだ。ただし、派生クラスで同じシグネチャーのコンストラクターがユーザー定義されている場合は、継承されない。
いくつか例を示す。
struct Base
{
Base( int ) { }
} ;
struct Derived : Base
{
using Base::Base ;
} ;
この場合、クラスDerivedのコンストラクターは、以下のようになる。
Derived( ) // 継承コンストラクターではない。使うとエラーになる
Derived( int ) // クラスBから継承されたコンストラクター
Derived( Derived const & ) // 継承コンストラクターではない
Derived( Derived && ) // 継承コンストラクターではない
デフォルトコンストラクターやコピー/ムーブコンストラクターは継承されないので、通常通りの挙動になる。この場合、クラスBaseのデフォルトコンストラクターは暗黙に宣言されていないし、ユーザー定義もされていないため、使うとエラーになる。
struct A
{
A( int ) { }
} ;
struct B
{
B( int ) { }
} ;
struct C : A, B
{
using A::A ;
using B::B ;
// エラー、宣言の重複
} ;
struct D : A, B
{
using A::A ;
using B::B ;
D( int x ) : A(x), B(x) { } // OK、ユーザー定義を優先
}
クラスCでは、C(int)を重複して宣言してしまうので、エラーとなる。クラスDでは、ユーザー定義があるために、コンストラクターの継承は起こらない。もっとも、この場合、using宣言を使ってコンストラクターを継承する意味がない。
関数と関数テンプレートは、異なる宣言であれば、同一スコープ内でも、同じ名前を使うことができる。これを、オーバーロード(overload)という。オーバーロードされた名前が関数呼び出しで使われた場合、オーバーロード解決(overload resolution)が行われ、最も最適な宣言が選ばれる。
void name( int ) ;
void name( double ) ;
int main()
{
name( 0 ) ; // name(int)
name( 0.0 ) ; // name(double)
}
これにより、引数が違うだけで本質的には同じ関数群に、それぞれ別名を付けなくてもよくなる。
シグネチャが異なっていれば、どのような関数、あるいは関数テンプレートでもオーバーロードできるわけではない。以下は、オーバーロードでは考慮されないシグネチャ上の違いである。
-
戻り値の型
int f( int ) { return 0 ; }
double f( int ) { return 0.0 ; } // エラー、オーバーロードできない
-
メンバー関数とメンバー関数テンプレートにおいて、staticと非staticの違い
struct Foo
{
void f() ;
static void f() ; // エラー
} ;
-
メンバー関数とメンバー関数テンプレートにおいて、リファレンス修飾子の有無が混在している場合
メンバー関数の暗黙のオブジェクト仮引数のリファレンスによるオーバーロードを行いたい場合は、lvalueリファレンスでも、リファレンス修飾子を省略することはできない。
struct Foo
{
void f() ; // リファレンス修飾子の省略、暗黙にlvalueリファレンス
void f() && ; // エラー、他の宣言でリファレンス修飾子が省略されている
void g() & ; // OK
void g() && ; // OK
} ;
-
仮引数の型が、同じ型を指す異なるtypedef名の場合
using Int = int ;
void f( int ) ;
void f( Int ) ; // 再宣言
typedef名は単なる別名であって、異なる型ではないので、シグネチャはおなじになる。
-
仮引数の型の違いが、*か[]である場合
関数の型で説明したように、仮引数のポインターと配列のシグネチャは同じである。ただし、2つ目以降の配列は考慮されるので注意。
void f( int * ) ;
void f( int [] ) ; // 再宣言、void f(int *)と同じ
void f( int [2] ) ; // 再宣言、void f(int *)と同じ
void f( int [][2] ) ; // オーバーロード、シグネチャはvoid f(int(*)[2])
-
仮引数が関数型か、同じ関数型へのポインターである場合
関数の型で説明したように、仮引数としての関数型は同じ関数型へのポインター型に変換される。
void f( void(*)() ) ;
void f( void () ) ; // 再宣言
void f( void g() ) ; // 再宣言
これらはオーバーロードではない。
-
仮引数のトップレベルのCV修飾子の有無
関数の型で説明したように、仮引数のトップレベルのCV修飾子は無視される。トップレベル以外のCV修飾子は別の型とみなされるので、オーバーロードとなる。
void f( int * ) ;
void f( int * const ) ; // 再宣言
void f( int * volatile ) ; // 再宣言
void f( int * const volatile ) ; // 再宣言
void f( int const * ) ; // オーバーロード
void f( int volatile * ) ; // オーバーロード
void f( int const volatile * ) ; // オーバーロード
-
デフォルト実引数の違い
デフォルト実引数の違いは、オーバーロードとはみなされない。
void f( int, int ) ;
void f( int, int = 0 ) ; // 再宣言
void f( int = 0, int = 0 ) ; // 再宣言
オーバーロード解決は、名前解決によって複数の宣言が列挙される場合に行われる。内側のスコープによって名前が隠されている場合は、オーバーロード解決は行われない。
たとえば、派生クラスで基本クラスのメンバー関数名と同名のものがある場合、そのメンバー関数は基本クラスのメンバー関数の名前を隠す。
struct Base
{
void f( int ) { }
} ;
struct Derived : Base
{
void f( double ) { } // Base::f(int)を隠す
} ;
int main()
{
Derived d ;
d.f( 0 ) ; // Derived::f(double)が呼ばれる
}
似たような例に、関数のローカル宣言がある。
void f( int ) { }
void f( double ) { }
int main()
{
f( 0 ) ; // f(int)を呼び出す
void f( double ) ; // f(int)を隠す
f( 0 ) ; // f(double)を呼び出す
}
オーバーロードされたメンバー関数は、それぞれ別々のアクセス指定を持つことができる。アクセス指定は名前解決には影響しないので、オーバーロード解決は行われる。
class X
{
private :
void f( int ) { }
public :
void f( double ) { }
} ;
int main()
{
X x ;
x.f( 0 ) ; // エラー、X::f(int)はprivateメンバー
}
この例では、オーバーロード解決によって、X::f(int)が選ばれるが、これはprivateメンバーなので、Xのfriendではないmain関数からは呼び出せない。よってエラーになる。
オーバーロードされた関数を呼び出す際に、実引数から判断して、最もふさわしい関数が選ばれる。これを、オーバーロード解決(Overload resolution)と呼ぶ。オーバーロード解決のルールは非常に複雑である。単純に実引数と仮引数の型が一致するだけならまだ話は簡単だ。
void f( int ) { }
void f( double ) { }
int main()
{
f( 0 ) ; // f(int)が呼ばれる
f( 0.0 ) ; // f(double)が呼ばれる
}
この結果には、疑問はない。実引数と仮引数の型が一致しているからだ。しかし、もし、実引数の型と仮引数の型が一致していないが、暗黙の型変換によって仮引数の型に変換可能な場合、問題は非常にややこしくなる。
void f( int ) { }
void f( double ) { }
int main()
{
short a = 0 ;
f( a ) ; // f(int)を呼ぶ
float b = 0.0f ;
f( b ) ; // f(double)を呼ぶ
}
この結果も、妥当なものである。shortは整数型なので、doubleよりはintを優先して欲しい。floatは、浮動小数点数型なので、doubleを優先して欲しい。
では、以下のような場合はどうだろうか。
void f( int ) { }
void f( long long ) { }
int main()
{
long a = 0l ;
f( a ) ; // 曖昧
short b = 0 ;
f( b ) ; // f(int)を呼び出す
}
この結果は、少し意外だ。比べるべき型は、intとlong long intである。long型を渡すと曖昧になる。しかし、short型を渡すと、なんとint型が選ばれる。こちらは曖昧にならない。これは、short型からint型への型変換に整数のプロモーションが使われているためである。
では、ユーザー定義の型変換が関係する場合はどうだろうか。
void f( int ) { }
class X
{
public :
X() = default ;
X( double ) { } // ユーザー定義の型変換
} ;
void f( X ) { }
int main()
{
f( 0.0 ) ; // f(int)を呼ぶ
}
この場合、ユーザー定義の型変換より、言語側に組み込まれた、標準型変換を優先している。
では、引数が複数ある場合はどうなるのか。関数テンプレートの場合はどうなるのか。疑問は尽きない。オーバーロード解決のルールは非常に複雑である。これは、できるだけオーバーロード解決の挙動を、人間にとって自然にし、詳細を知らなくても問題がないように設計した結果である。その代償として、オーバーロード解決の詳細は非常に複雑になり、実装にも手間がかかるようになった。
オーバーロード解決の手順を、簡潔にまとめると、以下のようになる。
- 名前探索によって見つかる同名の関数をすべて、候補関数(Candidate functions)として列挙する
- 候補関数から、実際に呼び出すことが可能な関数を、適切関数(Viable functions)に絞る
- 実引数から仮引数への暗黙の型変換を考慮して、最適な関数(Best viable function)を決定する
例えば、以下のようなオーバーロード解決の場合、
void f() { }
void f( int ) { }
void f( int, int ) { }
void f( double ) { }
void g( int ) { }
int main()
{
f( 0 ) ; // オーバーロード解決が必要
}
候補関数には、f(), f(int), f(int,int), f(double)が列挙される。適切関数には、f(int), f(double)が選ばれる。これを比較すると、f(int)が型一致で最適関数となる。
本書におけるオーバーロード解決の解説は、細部をかなり省略している。
候補関数(Candidate functions)は、正確に言えば、候補関数群とでも訳されるべきであろう。候補関数とは、その名前の通り、オーバーロード解決の際に呼び出しの優先順位を考慮される関数のことである。候補関数に選ばれなければ、呼び出されることはない。ある名前に対してオーバーロード解決が必要な場合に、まず最初に行われるのが、候補関数の列挙である。候補関数は、通常通りに名前探索をおこなって見つけた関数すべてである。これには、実際には呼び出すことのできない関数も含む。オーバーロード解決の際に考慮するのは、この候補関数だけである。その他の関数は考慮しない。
void f() { }
void f( int ) { }
void g() { }
int main()
{
f( 0 ) ; // 候補関数の列挙が必要
}
ここでの候補関数とは、f()とf(int)である。f()は、実際に呼び出すことができないが、候補関数として列挙される。この場合、g()は候補関数ではない。
オーバーロード解決の際に使われる名前探索は、通常の名前探索と何ら変わりないということに注意しなければならない。例えば、名前が隠されている場合は、発見されない。
void f( int ) { }
void f( double ) { }
int main()
{
f( 0 ) ; // #1 f(int)
void f( double ) ; // 再宣言、f(int)を隠す
f( 0 ) ; // #2 f(double)
}
#1では、f(int)が名前探索で見つかるので、オーバーロード解決によって、f(int)が最適関数に選ばれる。#2では、f(int)は隠されているので、名前探索では見つからない。そのため、f(int)は候補関数にはならない。結果として、f(double)が最適関数に選ばれる。
関数のローカル宣言はまず使われないが、派生クラスのメンバー関数の宣言によって、基本クラスのメンバー関数が隠されることはよくある。
struct Base
{
void f( int ) { }
void f( long ) { }
} ;
struct Derived : Base
{
void f( double ) { } // Baseクラスの名前fを隠す
void g()
{
f( 0 ) ; // Derived::f(double)
}
} ;
この例では、Derived::f(double)が、Baseのメンバー関数fを隠してしまうので、候補関数にはDerived::f(double)しか列挙されない。
候補関数がメンバー関数である場合、コード上には現れない仮引数として、クラスのオブジェクトを取る。これを、暗黙のオブジェクト仮引数(implicit object parameter)と呼ぶ。これは、オーバーロード解決の際に考慮される。暗黙のオブジェクト仮引数は、オーバーロード解決においては、関数の第一引数だとみなされる。暗黙のオブジェクト仮引数の型は、まず、クラスの型XにCV修飾子がつき、さらに、
リファレンス修飾子がない場合、あるいは、リファレンス修飾子が&の場合、X(場合によってCV修飾子)へのlvalueリファレンス。
struct X
{
// コメントは暗黙のオブジェクト仮引数の型
void f() & ; // X &
void f() const & ; // X const &
void f() volatile & ; // X volatile &
void f() const volatile & ; // X const volatile &
void g() ; // X &
} ;
リファレンス修飾子が&&の場合、X(場合によってCV修飾子)へのrvalueリファレンス。
struct X
{
// コメントは暗黙のオブジェクト仮引数の型
void f() && ; // X &&
void f() const && ; // X const &&
void f() volatile && ; // X volatile &&
void f() const volatile && ; // X const volatile &&
} ;
となる。例えば、以下のようにオーバーロード解決に影響する。
struct X
{
void f() & ; // #1 暗黙のオブジェクト仮引数の型は、X &
void f() const & ; // #2 暗黙のオブジェクト仮引数の型は、X const &
void f() && ; // #3 暗黙のオブジェクト仮引数の型は、X &&
} ;
int main()
{
X x ;
x.f() ; // #1
X const cx ;
cx.f() ; // #2
static_cast<X &&>(x).f() ; // #3
}
候補関数には、メンバー関数と非メンバー関数の両方を含むことがある。
struct X
{
X operator + ( int ) const
{ return X() ; }
} ;
X operator + ( X const &, double )
{ return X() ; }
int main()
{
X x ;
x + 0 ; // X::operator+(int)
x + 0.0 ; // operator+(X const &, double)
}
この場合、候補関数には、メンバー関数であるX::operator +と、非メンバー関数であるoperator+の両方が含まれる。候補関数に列挙されるので、当然、オーバーロード解決で最適関数が決定される。
テンプレートの実引数推定は、名前解決の際に行われる。そのため、候補関数として関数テンプレートのインスタンスが列挙された時点で、テンプレート実引数は決定されている。
オーバーロード解決が行われる文脈には、いくつか種類がある。それによって、候補関数の選び方も違ってくる。
最も分かりやすい関数呼び出しは、関数呼び出しの文法によるものだろう。しかし、一口に関数呼び出しの文法といっても、微妙に違いがある。単なる関数名に対する関数呼び出し式の適用もあれば、クラスのオブジェクトに.や->を使った式に対する関数呼び出し、つまりメンバー関数の呼び出しや、クラスのオブジェクトに対する関数呼び出し式、つまりoperator ()のオーバーロードを呼び出すものがある。
struct X
{
void f( int ) { }
void f( double ) { }
void operator () ( int ) { }
void operator () ( double ) { }
} ;
int main()
{
X x ;
x.f( 0 ) ; // オーバーロード解決が必要
x( 0 ) ; // オーバーロード解決が必要
}
オーバーロード解決は、関数へのポインターやリファレンスを経由した間接的な呼び出しの際には、行われない。
void f( int ) { }
void f( double ) { }
int main()
{
void (* p)( int ) = &f ;
p( 0.0 ) ; // f(int)
}
この項は、オーバーロードされた演算子を候補関数として見つける際の詳細である。演算子のオーバーロードの宣言方法については、オーバーロードされた演算子を参照。
演算子を使った場合にも、オーバーロード解決が必要になる。ただし、演算子にオーバーロード解決が行われる場合、オペランドにクラスやenumが関わっていなければならない。オペランドが基本型だけであれば、組み込みの演算子が使われる。
// エラー、オペランドがすべて基本型
int operator + (int, int) { return 0 ; }
演算子のオーバーロードは、メンバー関数としてオーバーロードする方法と、非メンバー関数としてオーバーロードする方法がある。すでに述べたように、候補関数には、どちらも列挙される。
演算子のオーバーロード関数は、演算子を仮に@と置くと、以下の表のように呼ばれる。
種類 |
式 |
メンバー関数として呼び出す場合 |
非メンバー関数として呼び出す場合 |
単項前置 |
@a |
(a).operator@ ( ) |
operator@ (a) |
単項後置 |
a@ |
(a).operator@ (0) |
operator@ (a, 0) |
二項 |
a@b |
(a).operator@ (b) |
operator@ (a, b) |
代入 |
a=b |
(a).operator= (b) |
添字 |
a[b] |
(a).operator[](b) |
クラスメンバーアクセス |
a-> |
(a).operator-> ( ) |
代入、添字、クラスメンバーアクセスの演算子は、メンバー関数として宣言しなければならないので、非メンバー関数は存在しない。
クラスのオブジェクトの直接初期化の場合、そのクラスからコンストラクターが候補関数として列挙され、オーバーロード解決が行われる。
struct X
{
X( int ) { }
X( double ) { }
} ;
int main()
{
X a( 0 ) ; // オーバーロード解決が行われる
X b( 0.0 ) ; // オーバーロード解決が行われる
}
クラスのコピー初期化におけるユーザー定義型変換には、オーバーロード解決が行われる。ユーザー定義型変換には、変換コンストラクターと変換関数がある。これは、両方とも、候補関数として列挙される。
struct Destination ;
extern Destination obj ;
struct Source
{
operator Destination &() { return obj ; }
} ;
struct Destination
{
Destination() { }
Destination( Source const & ) { }
} ;
Destination obj ;
int main()
{
Source s ;
Destination d ;
d = s ; // オーバーロード解決、Source::operator Destination &()
Source const cs ;
d = cs ; // オーバーロード解決、Destination::Destination( Source const & )
}
この例では、変換コンストラクターと変換関数の両方が候補関数として列挙される。この例で、もし変換コンストラクターの仮引数が、Source &ならば、オーバーロード解決は曖昧になる。
ただし、explicit変換コンストラクターとexplicit変換関数は、直接初期化か、明示的なキャストが使われた際にしか候補関数にならない。
struct X
{
X() { }
explicit X( int ) { }
explicit operator int() { return 0 ; }
} ;
int main()
{
X x ;
int a( x ) ; // OK
int b = x ; // エラー
X c( 0 ) ; // OK
X d = 0 ; // エラー
}
この場合の実引数リストには、初期化式が使われる。変換コンストラクターの場合は、第一仮引数と比較され、変換関数の場合は、クラスの隠しオブジェクト仮引数と比較される。
// 変換コンストラクターの例
struct A { } ;
struct X
{
// 候補関数
X( A & ) { }
X( A const & ) { }
} ;
int main()
{
A a ;
X x1 = a ; // オーバーロード解決、A::A(A&)
A const ca ;
X x2 = ca ; // オーバーロード解決、A::A(A const &)
}
この例では、実引数としてaやcaが使われ、クラスXの変換コンストラクターの第一仮引数と比較される。
// 変換関数の例
struct A { } ;
struct X
{
// 候補関数
operator A() & { return A() ; }
operator A() const & { return A() ; }
operator A() && { return A() ; }
} ;
int main()
{
X x ;
// オーバーロード解決、X::operator A() &
// 実引数はlvalueのX
A a1 = x ;
X const cx ;
// オーバーロード解決、X::operator A() const &
// 実引数はconstなlvalue
A a2 = cx ;
// オーバーロード解決、X::operator A() &&
// 実引数はxvalue
A a3 = static_cast<X &&>(x) ;
}
この例では、クラスXのオブジェクトが実引数として、変換関数のクラスの隠しオブジェクト仮引数として比較される。たとえば、A a1 = x ; の場合、実引数は非constなlvalueなので、オーバーロード解決により、X::operator A() &が選ばれる。
その他の変換コンストラクターと変換関数に対しても、オーバーロード解決で比較する実引数と仮引数はこれに同じ。
クラスではないオブジェクトを、クラスのオブジェクトの初期化式で初期化する際、クラスの変換関数が候補関数として列挙され、オーバーロード解決が行われる。実引数リストには、初期化式がひとつの実引数として渡される。
struct X
{
operator int() { return 0 ; }
operator long() { return 0L ; }
operator double() { return 0.0 ; }
} ;
int main()
{
X x ;
int i = x ; // オーバーロード解決が行われる
}
この例では、候補関数に、X::operator int、X::operator long、X::operator doubleが列挙され、オーバーロード解決によってX::operator intが選ばれる。
リファレンスを初期化するとき、初期化式に変換関数を適用して、その結果を束縛できる。このとき、クラスの変換関数が候補関数として列挙され、オーバーロード解決が行われる。
struct X
{
operator int() { return 0 ; }
operator short() { return 0 ; }
} ;
int main()
{
X x ;
int && ref = x ; // オーバーロード解決、X::operator int()
}
アグリゲートではないクラスがリスト初期化によって初期化されるとき、オーバーロード解決によってコンストラクターが選択される。
この際の候補関数の列挙は、二段階に分かれている。
まず一段階に、クラスの初期化リストコンストラクターが候補関数として列挙され、オーバーロード解決が行われる。実引数リストには、初期化リストが唯一の実引数として、std::initializer_list<T>の形で、与えられる。
struct X
{
// 初期化リストコンストラクター
X( std::initializer_list<int> ) { }
X( std::initializer_list<double> ) { }
// その他のコンストラクター
X( int, int, int ) { }
X( double, double, double ) { }
} ;
int main()
{
X a = { 1, 2, 3 } ; // オーバーロード解決、X::X( std::initializer_list<int> )
X b = { 1.0, 2.0, 3.0 } ; // オーバーロード解決、X::X( std::initializer_list<double> )
}
この場合、候補関数には、初期化リストコンストラクターしか列挙されない。
もし、一段階目の名前解決で、適切な初期化リストコンストラクターが見つからなかった場合、二段階の候補関数として、再びオーバーロード解決が行われる。今度は、クラスのすべてのコンストラクターが候補関数として列挙される。実引数は、初期化リストの中の要素が、それぞれ別の実引数として渡される。
struct X
{
// 適切な初期化リストコンストラクターなし
X( int, int, int ) { }
X( double, double, double ) { }
X( int, double, int ) { }
} ;
int main()
{
X a = { 1, 2, 3 } ; // オーバーロード解決、X::X( int, int, int )
X b = { 1.0, 2.0, 3.0 } ; // オーバーロード解決、X::X( double, double, double )
X c = { 1, 2.0, 3 } ; // オーバーロード解決、X::X( int, double, int )
}
「適切」という用語に注意すること。もし、縮小変換が必要となれば、適切関数かどうかを判定する前にエラーとなる。
struct X
{
X( std::initializer_list<int> ) { }
X( double, double, double ) { }
} ;
int main()
{
X b = { 1.0, 2.0, 3.0 } ; // エラー、縮小変換が必要
}
デフォルトコンストラクターを持つクラスに空の初期化リストが渡された場合、一段階目のオーバーロード解決は行われず、デフォルトコンストラクターが呼ばれる。
struct X
{
X( ) { }
template < typename T >
X( std::initializer_list<T> ) { }
} ;
int main()
{
X x = { } ; // デフォルトコンストラクターが呼ばれる
}
コピーリスト初期化では、explicitコンストラクターが選ばれた場合、エラーとなる。
struct X
{
explicit X( int ) { }
} ;
int main()
{
X a = { 0 } ; // エラー、コピーリスト初期化でexplicitコンストラクター
X b{ 0 } ; // OK、直接初期化
}
候補関数は、単に名前探索の結果であり、実際には呼び出すことができない関数も含まれている。このため、候補関数を列挙した後、呼び出すことが出来る関数、すなわち適切関数(Viable functions)を列挙する。
適切関数とは、与えられた実引数で、実際に呼び出すことが出来る関数である。これには、大きく二つの要素がある。仮引数の数と型である。
適切関数となるためにはまず、与えられた実引数の個数に対して、仮引数の個数が対応していなければならない。そのための条件は、以下のいずれかを満たしていればよい。
-
実引数の個数と、候補関数の仮引数の個数が一致する関数
これは簡単だ。実引数と同じ個数だけの仮引数があればよい。可変長テンプレートのインスタンス化による関数もこのうちに入る。
void f( int, int ) { }
int main()
{
f( 0, 0 ) ; // OK
f( 0 ) ; // エラー
}
-
候補関数の仮引数の個数が、実引数の個数より少ないが、仮引数リストにエリプシス(...)がある場合。
これは、C言語でお馴染みの...のことだ。可変長テンプレートは、このうちには入らない。
void f( int, ... ) ;
int main()
{
f( 0 ) ; // 適切関数
f( 0, 1 ) ; // 適切関数
f( 0, 1, 2, 3, 4, 5 ) ; // 適切関数
}
-
候補関数の仮引数の個数は、実引数より多いが、実引数より多い仮引数にはすべて、デフォルト実引数が指定されていること。
void f( int, int = 0, int = 0, int = 0, int = 0, int = 0 ) ;
int main()
{
f( 0 ) ; // 適切関数
f( 0, 1 ) ; // 適切関数
f( 0, 1, 2, 3, 4, 5 ) ; // 適切関数
}
さらに、対応する実引数から仮引数に対して、後述する暗黙の型変換により、妥当な変換が存在しなければならない。
void f( int ) { }
int main()
{
f( 0 ) ; // OK、完全一致
f( 0L ) ; // OK、整数変換
f( 0.0 ) ; // OK、整数と浮動小数点数間の変換
f( &f ) ; // エラー
}
適切関数であるからといって、実際に呼び出せるとは限らない。たとえば、宣言されているが未定義であったり、アクセス指定による制限を受けたり、あるいはその他実装依存の理由など、現実には呼び出すことができない理由は多数存在する。
適切関数が複数ある場合、定められた方法で関数を比較することによって、ひとつの最も適切(best viable)な関数を選択する。この関数を最適関数と呼ぶ。オーバーロード解決の結果は、この最適関数となる。もし、最も適切な関数をひとつに決定できない場合、オーバーロード解決は曖昧であり、エラーとなる。
最適関数の決定は、主に、後述する暗黙の型変換の優先順位によって決定される。
まず大前提として、ある関数が、別の関数よりも、より適切であると判断されるには、ある関数のすべて仮引数に対する実引数からの暗黙の型変換の優先順位が劣っておらず、かつ、ひとつ以上の優れている型変換が存在しなければならない。
void f( int, double ) { } // #1
void f( long, int ) { } // #2
int main()
{
f( 0 , 0 ) ; // エラー、オーバーロード解決が曖昧
}
この例では、どの関数も、仮引数への型変換の優先順位が、他の関数より劣っている。したがってオーバーロード解決は曖昧となる。一見すると、#2の方が、どちらも整数型であるので、よりよい候補なのではないかと思うかもしれない。しかし、#1の第一仮引数の型はintなので、longよりも優れている。一方、第二引数では、#2の方が優れている。このため、曖昧となる。最適関数となるためには、全ての仮引数の型が、他の候補より劣っていてはならないのだ。
ユーザー定義型変換による初期化の場合、ユーザー定義型変換の結果の型から、目的の型へ、標準型変換により変換する際、より優先順位の高いものが選ばれる。
struct X
{
operator int() ;
operator double() ;
} ;
void f()
{
X x ;
int i = x ; // operator intが最適関数
float f = x ; // エラー、曖昧
}
一見すると、doubleからfloatへの変換は、intからの変換より優先順位が高いのではないかと思うかもしれないが、後述する標準型変換の優先順位のルールにより、同じ優先順位なので、曖昧となる。
非テンプレート関数と関数テンプレートの特殊化では、非テンプレート関数が優先される。
template < typename T >
void f( T ) ;
void f( int ) ;
int main()
{
f( 0 ) ; // 非テンプレート関数を優先
}
もちろん、これは大前提の、すべての仮引数に対し劣った型変換がないということが成り立つ上での話である。
template < typename T >
void f( T ) ;
void f( long ) ;
int main()
{
f( 0 ) ; // 関数テンプレートの特殊化f<int>を優先
}
この場合は、テンプレートの特殊化である仮引数int型の方が、実引数int型に対して、より優れた型変換なので、優先される。
テンプレートの実引数推定のルールは複雑なので、一見して、非テンプレート関数が優先されると思われるコードで、関数テンプレートの実体化の方が優先される場合がある。
// #1
// 非テンプレート関数
void f( int const & ) ;
// #2
// 関数テンプレート
template < typename T >
void f( T && ) ;
int main()
{
int x = 0 ; // xは非constなlvalue
f( x ) ; // #2を呼ぶ
}
これは、#2の実体化の結果が、f<int &>( int & )になるからだ。xは非constなlvalueであるので、非constなlvalueリファレンス型の仮引数と取る#2の方が優先される。
ふたつの関数が両方ともテンプレートの特殊化の場合、半順序によって、より特殊化されていると判断される方が、優先される。
template < typename T > void f( T ) ; // #1
template < typename T > void f( T * ) ; // #2
int main()
{
int * ptr = nullptr ;
f( ptr ) ; // 半順序により#2を優先
}
#1と#2の特殊化による仮引数の型は、どちらも int *であるが、#2のテンプレートの特殊化の方が、半順序のルールによって、より特殊化されているとみなされるため、#2が優先される。
暗黙の型変換には、いくつかの種類と、多数の例外ルールがあり、それぞれ優先順位を比較することができる。残念ながら、この詳細は非常に冗長であり、本書では概略の説明に留める。
まず、暗黙の型変換には、大別して三種類ある。標準型変換、ユーザー定義型変換、エリプシス変換である。優先順位もこの並びである。標準型変換が一番優先され、次にユーザー定義型変換、最後にエリプシス変換となる。
struct X { X(int) ; } ;
void f( long ) ; // #1
void f( X ) ; // #2
void g( X ) ; // #3
void g( ... ) ; // #4
int main()
{
f( 0 ) ; // #1、標準型変換がユーザー定義型変換に優先される
g( 0 ) ; // #3、ユーザー定義型変換がエリプシス変換に優先される
}
さらに、標準型変換とユーザー定義変換同士の間での優先順位がある。
エリプシスに基本型以外を渡して呼び出した場合の挙動は未定義だが、オーバーロード解決には影響しない。
オーバーロード解決における標準型変換の間の優先順位は、非常に複雑で、単に、ランクA>ランクBのような単純な比較ができない。ここでは、とくに問題になりそうな部分のみ取り上げる。
まず、型変換の必要のない、完全一致が最も優先される。
void f( int ) ;
void f( double ) ;
int main()
{
f( 0 ) ; // f(int)
f( 0.0 ) ; // f(double)
}
この完全一致には、lvalueからrvalueへの型変換、配列からポインターへの型変換、関数からポインターへの型変換が含まれる。
void f( int ) ;
int main()
{
int x = 0 ;
f( x ) ; // lvalueからrvalueへの変換
}
配列や関数からポインターへの変換は、完全一致とみなされることに注意。
void g( ) ;
void f( void (*)() ) ; // ポインター
void f( void (&)() ) ; // リファレンス
int main()
{
f( g ) ; // エラー、オーバーロード解決が曖昧、候補関数はすべて完全一致
f( &g ) ; // OK、f( void (*)() )
}
完全一致は、ポインターやリファレンスにCV修飾子を付け加える型変換より優先される。
void f( int & ) ; // #1
void f( int const & ) ; // #2
int main()
{
int x = 0 ;
f( x ) ; // #1、完全一致
}
整数と浮動小数点数のプロモーションは、その他の整数と浮動小数点数への変換より優先される。
void f( int ) ;
void f( long ) ;
int main()
{
short x = 0 ;
f( x ) ; // f(int)、プロモーション
}
ある関数の名前に対して、複数の候補関数がある場合でも、名前から関数のアドレスを取得できる。どの候補関数を選ぶかは、文脈が期待する型の完全一致で決定される。初期化や代入、関数呼び出しの実引数や明示的なキャストの他に、関数の戻り値も、文脈により決定される。
void f( int ) ;
void f( long ) ;
void g( void (*)(int) ) ;
void h()
{
// 初期化
void (*p)(int) = &f ; // void f(int)のアドレス
// 代入
p = &f ; // void f(int)のアドレス
// 関数呼び出しの実引数
g( &f ) ;
// 明示的なキャスト
static_cast<void (*)(int)>(&f) ; // void f(int)のアドレス
}
// 関数の戻り値
auto i() -> void (*)(int)
{
return &f ; // void f(int)のアドレス
}
これらの文脈では、ある具体的な完全一致の型を期待しているので、オーバーロードされた関数名から、適切な関数を決定できる。
完全一致の型ではない場合や、型を決定できない場合はエラーである。
void f( int ) ;
void f( long ) ;
template < typename T >
void g( T ) { }
int main()
{
g( &f ) ; // エラー
}
特別な識別子を使っている関数宣言は、演算子関数(operator function)として認識される。この識別子は以下のようになる。
operator 演算子
オーバーロード可能な演算子は以下の通りである。
new delete new[] delete[]
+ - * / % ˆ & | ~
! = < > += -= *= /= %=
ˆ= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
( ) [ ]
以下の演算子は、単項、二項の両方でオーバーロードできる。
+ - * &
以下の演算子は、関数呼び出しと添え字である。
( ) [ ]
以下の演算子は、オーバーロードできない。
. .* :: ?:
new, new[], delete, delete[]については、確保関数と解放関数も参照。
演算子関数は、非staticメンバー関数か、非メンバー関数でなければならない。非staticメンバー関数の場合、暗黙のオブジェクト仮引数が、第一オペランドになる。これが*thisである。
非メンバー関数の場合、仮引数のひとつは、クラスか、クラスへのリファレンス、enumかenumへのリファレンスでなければならない。
struct X
{
// 非staticメンバー関数による演算子関数
X operator +() const ; // 暗黙のオブジェクト仮引数 X const &
X operator +( int ) const ; // 暗黙のオブジェクト仮引数 X const &
} ;
// 非メンバー関数による演算子関数
X operator -( X const & ) ;
X operator -( X const &, int ) ;
X operator -( int, X const & ) ;
以下の例はエラーである。
// エラー、組み込みの演算子をオーバーロードできない
int operator +( int, int ) ;
struct X { } ;
// エラー、組み込みの演算子をオーバーロードできない
X operator + ( X * ) ;
ただし、代入演算子や添字演算子のように、非staticメンバー関数として実装しなければならない例外的な演算子もある。
演算子関数は、必ず元の演算子と同じ数の仮引数を取らなければならない。
struct X { } ;
X operator / ( X & ) ; // エラー、仮引数が少ない
X operator / ( X &, X &, X & ) ; // エラー、仮引数が多い
ただし、これも関数呼び出し演算子のように、例外的な演算子がある。
演算子関数は、組み込みの演算子と同じ挙動を守らなくてもよい。例えば、戻り値の型は自由であるし、オーバーロードされた演算子関数が、基本型にその単項演算子を適用した場合に期待される挙動をしなくてもかまわない。例えば、オーバーロードした演算子関数では、"++a"、と、"a += 1"というふたつの式を評価した際の挙動や結果が同じにならなくてもよい。また、組み込み演算子ならば非constなlvalueを渡す演算子で、constなlvalueやrvalueを受け取っても構わない。
struct X { } ;
void operator + ( X & ) ; // OK、戻り値の型は自由
void operator ++ ( X const & ) ; // OK、constなlvalueリファレンスでもよい
演算子関数は、通常通り演算子を使うことによって呼び出すことができる。その際、演算子の優先順位は、組み込みの演算子と変わらない。また、識別子を指定することによって、通常の関数呼び出し式の文法で、明示的に呼び出すこともできる。
struct X
{
X operator +( X const & ) const ;
X operator *( X const & ) const ;
} ;
int main()
{
X a ; X b ; X c ;
a + b ; // 演算子を使うことによる呼び出し
a + b * c ; // 優先順位は、(a + (b * c))
a.operator +(b) ; // 明示的な関数呼び出し
}
代入演算子=や、単項演算子の&や、カンマ演算子は、オーバーロードしなくてもすべての型に対してあらかじめ定義された挙動がある。この挙動はオーバーロードして変えることもできる。
オーバーロード可能な単項演算子は、以下の通りである。
+ - * & ~ !
ここでは、*と&は単項演算子であることに注意。二項演算子の項も参照。
インクリメント演算子とデクリメント演算子については、インクリメントとデクリメントを参照。
単項演算子は、演算子を@とおくと、@xという式は、非staticメンバー関数の場合、x.operator @()、非メンバー関数の場合、operator @(x)として呼び出される。単項演算子では、非staticメンバー関数と非メンバー関数は、機能的に違いはない。
struct X
{
void operator + () ;
} ;
void operator -( X & ) ;
int main()
{
X x ;
+x ; // x.operator + ()
-x ; // operator - (x)
}
非staticメンバー関数の場合、明示的に仮引数をとらない。暗黙のオブジェクトが仮引数として渡される。
struct X
{
void operator + () & ;
void operator + () const & ;
void operator + () volatile & ;
void operator + () const volatile & ;
void operator + () && ;
void operator + () const && ;
void operator + () volatile && ;
void operator + () const volatile && ;
} ;
int main()
{
X x ;
+x ; // void operator + () &
+static_cast<X &&>(x) ; // void operator + () &&
X const cx ;
+x ; // void operator + () const &
}
同様のコードを、非メンバー関数として書くと、以下のようになる。
struct X { } ;
void operator + ( X & ) ;
void operator + ( X const & ) ;
void operator + ( X volatile & ) ;
void operator + ( X const volatile & ) ;
void operator + ( X && ) ;
void operator + ( X const && ) ;
void operator + ( X volatile && ) ;
void operator + ( X const volatile && ) ;
int main()
{
X x ;
+x ; // void operator + ( X & )
+static_cast<X &&>(x) ; // void operator + ( X && )
X const cx ;
+x ; // void operator + ( X const & )
}
また、非メンバー関数の場合は、クラス型を引数に取ることができる。
struct X { } ;
void operator + ( X ) ;
operator &には、注意を要する。これは、組み込みの演算子、すなわち、オペランドのアドレスを得る演算子として、すべての型にあらかじめ定義されている。
// operator &のオーバーロードなし
struct X { } ;
int main()
{
X x ;
X * ptr = &x ; // 組み込みのoperator &の呼び出し
}
この演算子をオーバーロードすると、組み込みのoperator &が働かなくなる。
struct X
{
X * operator &() { return nullptr ; }
} ;
int main()
{
X x ;
X * ptr = &x ; // 常にnullポインターになる。
}
もちろん、戻り値の型は自由だから、なにか別のことをさせるのも可能だ。
class int_wrapper
{
private :
int obj ;
public :
int * operator &() { return &obj ; }
} ;
int main()
{
int_wrapper wrap ;
int * ptr = &wrap ;
}
ただし、クラスのユーザーが、オブジェクトのアドレスを得たい場合、組み込みの演算子を呼び出すのは簡単ではない。そのため、標準ライブラリヘッダー<memory>には、std::addressofという関数テンプレートが定義されている。これを使えば、operator &がオーバーロードされているクラスでも、クラスのオブジェクトのアドレスを得ることができる。
struct X
{
void operator &() { }
} ;
int main()
{
X x ;
X * p1 = &x ; // エラー、operator &の戻り値の型はvoid
X * ptr = std::addressof(x) ; // OK
}
オーバーロード可能な二項演算子は以下の通りである。
+ - * / % ^ & | ~
! < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ,
代入演算子は特別な扱いを受ける。詳しくは、代入演算子を参照。複合代入演算子は、二項演算子に含まれる。
二項演算子は、演算子を@とおくと、x@yという式に対して、非staticメンバー関数の場合、x.operator @(y)、非メンバー関数の場合、operator @(x,y)のように呼び出される。
struct X
{
void operator + (int) const ;
} ;
void operator - ( X const &, int ) ;
int main()
{
X x ;
x + 1 ; // x.operator +(1)
x - 1 ; // operator -(x, 1)
}
非staticメンバー関数の場合、第一オペランドが暗黙のオブジェクト仮引数に、第二オペランドが実引数に渡される。
struct X
{
void operator + (int) & ;
void operator + (int) const & ;
void operator + (int) volatile & ;
void operator + (int) const volatile & ;
void operator + (int) && ;
void operator + (int) const && ;
void operator + (int) volatile && ;
void operator + (int) const volatile && ;
} ;
int main()
{
X x ;
x + 1 ; // X::operator + (int) &
static_cast<X &&>(x) + 1 ; // X::operator + (int) &&
X const cx ;
cx + 1 ; // X::operator + (int) const &
}
同様のコードを、非メンバー関数で書くと以下のようになる。
struct X { } ;
void operator + ( X &, int) ;
void operator + ( X const &, int) ;
void operator + ( X volatile &, int) ;
void operator + ( X const volatile &, int) ;
void operator + ( X &&, int) ;
void operator + ( X const &&, int) ;
void operator + ( X volatile &&, int) ;
void operator + ( X const volatile &&, int) ;
int main()
{
X x ;
x + 1 ; // operator + ( X &, int)
static_cast<X &&>(x) + 1 ; // operator + ( X &&, int)
X const cx ;
cx + 1 ; // operator + ( X const &, int)
}
非メンバー関数の場合は、クラス型を仮引数に取ることができる。
struct X { } ;
void operator + ( X, int ) ;
第二オペランドにクラスやenum型、あるいはそのリファレンス型を取りたい場合は、非メンバー関数しか使えない。
struct X { } ;
void operator + ( int, X & ) ;
int main()
{
X x ;
1 + x ;
}
メンバー関数によるオーバーロードでは、必ず第一オペランドのメンバーとして演算子関数がよばれるので、これはできない。
カンマ演算子、operator ,には、あらかじめ定義された組み込みの演算子が存在する。オーバロードにより、この挙動を変えることもできる。ただし、operator ,の挙動を変えるのは、ユーザーを混乱させるので、慎むべきである。もし、単に任意個の引数を取りたいというのであれば、可変長テンプレートや初期化リストなどの便利な機能が他にもある。
代入演算子のオーバーロードは、仮引数をひとつとる非staticメンバー関数として実装する。非メンバー関数として実装することはできない。複合代入演算子は、代入演算子ではなく、二項演算子である。
struct X
{
// コピー代入演算子
X & operator = ( X const & ) ;
// ムーブ代入演算子
X & operator = ( X && ) ;
// intからの代入演算子
X & operator = ( int ) ;
} ;
// エラー、非メンバー関数として宣言することはできない
X & operator = ( X &, double ) ;
// OK、複合代入演算子は二項演算子
X & operator += ( X &, double ) ;
もちろん、戻り値の型は自由である。ただし、慣例として、暗黙に定義される代入演算子は、*thisを返すようになっている。詳しくは、クラスオブジェクトのコピーとムーブを参照。
関数呼び出し演算子の識別子は、operator ()である。関数呼び出し演算子のオーバーロードは、任意個の仮引数を持つ非staticメンバー関数として宣言する。非メンバー関数として宣言することはできない。デフォルト実引数も使うことができる。
関数呼び出し演算子は、x(arg1, ...)とおくと、x.operator()(arg1, ...)のように呼び出される。
struct X
{
void operator () ( ) ;
void operator () ( int ) ;
void operator () ( int, int, int = 0 ) ;
} ;
int main()
{
X x ;
x() ; // x.operator () ( )
x( 0 ) ; // x.operator () ( 0 )
x( 1, 2 ) ; // x.operator() ( 1, 2 )
}
添字演算子の識別子は、operator []である。添字演算子のオーバーロードは、ひとつの仮引数を持つ非staticメンバー関数として宣言する。非メンバー関数として宣言することはできない。
添字演算子は、x[y]とおくと、x.operator [] (y)のように呼び出される。
struct X
{
void operator [] ( int ) ;
} ;
int main()
{
X x ;
x[1] ; // x.operator [] (1)
}
添字演算子に複数の実引数を渡すことはできない。ただし、初期化リストならば渡すことができる。
struct X
{
void operator [] ( std::initializer_list<int> list ) ;
} ;
int main()
{
X x ;
x[ { 1, 2, 3 } ] ;
}
クラスメンバーアクセス演算子の識別子は、operator ->である。クラスメンバーアクセス演算子は仮引数を取らない非staticメンバー関数として宣言する。非メンバー関数にすることはできない。クラスメンバーアクセス演算子は、後述するように、少し変わった特徴がある。
クラスメンバーアクセス演算子は、x->mとおくと、(x.operator->())->mのように呼び出される。つまり、もし、x.operator->()の戻り値の型がクラスへのポインターであれば、そのまま組み込みのクラスメンバーアクセス演算子が使われる。それ以外の場合は、戻り値に対してクラスメンバーアクセス演算子を適用しているために、さらに戻り値のクラスメンバーアクセス演算子が、もし存在すれば、呼び出される。
struct A
{
int member ;
} ;
struct B
{
A a ;
A * operator ->() { return &a ; }
} ;
struct C
{
B b ;
B & operator ->() { return b ; }
} ;
int main()
{
B b ;
b->member ; // (b.operator ->())->member
C c ;
// (c.operator ->())->member
// すなわちこの場合、以下のように展開される。
// ((c.operator ->()).operator ->())->member
c->member ;
}
クラスBは、
クラスCのoperator ->がB &型を返していることに注目。lvalueのBにクラスメンバーアクセス演算子である->が使われるため、クラスBのクラスメンバーアクセス演算子が呼ばれる。
クラスメンバーアクセス演算子の評価の結果に対するクラスメンバーアクセス演算子の呼び出しは、際限なく行われる。このループを断ち切るには、最終的にクラスへのポインターを返し、組み込みのクラスメンバーアクセス演算子を使わなければならない。
もちろん、これは演算子として使用した場合であって、明示的に関数を呼び出す場合には、通常通り、その関数だけが呼ばれる。もちろん、戻り値の型をvoid型にすることもできる。
struct X
{
void operator ->() { }
} ;
int main()
{
X x ;
x.operator ->() ; // OK
}
インクリメント演算子の識別子はoperator ++、デクリメント演算子の識別子はoperator --である。インクリメント演算子とデクリメントの演算子は非staticメンバー関数と、非メンバー関数の両方で宣言できる。インクリメント演算子とデクリメント演算子は、識別子の違いを除けば、同じように動く。ここでのサンプルコードは、インクリメント演算子の識別子を使う。
インクリメントとデクリメントには、前置と後置の違いがある。
++a ; // 前置
a++ ; // 後置
前置演算子は、非staticメンバー関数の場合、仮引数を取らない。非メンバー関数の場合は、ひとつの仮引数を取る。
前置演算子は、++xという式に対して、非staticメンバー関数の場合、x.operator ++ ()、非メンバー関数の場合、operator ++( x )のように呼び出される。
struct X
{ // 非staticメンバー関数の例
void operator ++ () ;
} ;
struct Y { } ;
// 非メンバー関数の例
void operator ++ ( Y & ) ;
int main()
{
X x ;
++x ; // x.operator ++()
Y y ;
++y ; // operator ++(y)
}
後置演算子は、非staticメンバー関数の場合、int型の引数を取る。非メンバー関数の場合は、二つの仮引数を取る。第二仮引数の型はintでなければならない。int型の仮引数は、単に前置と後置を別の宣言にするためのタグであり、それ以上の意味はない。式としてインクリメントとデクリメントを使うと、実引数には0が渡される。
後置演算子は、x++という式に対して、非staticメンバー関数の場合、x.operator ++( 0 ), 非メンバー関数の場合、operator ++ ( x, 0 )のように呼び出される。
struct X
{ // 非staticメンバー関数の例
void operator ++ (int) ;
} ;
struct Y { } ;
// 非メンバー関数の例
void operator ++ ( Y & , int ) ;
int main()
{
X x ;
x++ ; // x.operator ++( 0 )
Y y ;
y++ ; // operator ++( y, 0 )
}
intをタグとして使うこの仕様はすこし汚いが、例外的な文法を使わなくてもよいという利点があるので採用された。もし明示的に呼び出した場合は、int型の仮引数に対し、0以外の実引数を与えることもできる。
ここでは確保関数と解放関数のオーバーロードについて解説している。
注意:本来、これはコア言語ではなくライブラリで規定されていることなので、本書の範疇ではないのだが、ここでは読者の便宜のため、宣言方法と、デフォルトの挙動のリファレンス実装を提示する。また、サンプルコードは分割して掲載しているが、確保関数と解放関数はそれぞれ関係しており、すべて一つのソースファイルに含まれることを想定している。そのため、ヘッダーファイルのincludeは最初のサンプルコードにしか書いていない。
確保関数の識別子はoperator newである。解放関数の識別子はoperator deleteである。この関数は、動的ストレージの確保と解放を行う。確保関数と解放関数が行うのは、生の動的ストレージの確保と解放である。よく誤解があるが、コンストラクターやデストラクターの呼び出しの責任は持たない。
確保関数と解放関数のオーバーロードは、グローバル名前空間か、クラスのメンバー関数として宣言する。グローバル名前空間以外の名前空間で宣言するとエラーとなる。確保関数と解放関数がユーザー定義されない場合、実装によってデフォルトの挙動を行う確保関数と解放関数が自動的に定義される。
// グローバル名前空間
void* operator new(std::size_t size) ; // OK
namespace NS
{
void* operator new(std::size_t size); // エラー、グローバル名前空間ではない
}
struct X
{
void* operator new(std::size_t size) ; // OK
} ;
グローバル名前空間の宣言は、デフォルトの確保関数と解放関数の生成を妨げる。クラスのメンバー関数は、そのクラスと派生クラスの確保と解放に使われる。
確保関数には、効果(effect)と必須の挙動(required behavior)とデフォルトの挙動(default behavior)が規定されている。解放関数には、効果とデフォルトの挙動が規定されている。効果とは、その関数がどのようなことに使われるのかという規定である。必須の挙動とは、たとえユーザー定義の関数であっても必ず守らなければならない挙動のことである。デフォルトの挙動とは、関数がユーザー定義されていない場合、実装によって用意される定義の挙動である。
C++11ではスレッドの概念が入ったので、確保関数と解放関数は、データ競合を引き起こしてはならない。この保証は、ユーザー定義の確保関数と解放関数にも要求される。
C++11ではアライメントの概念が入ったので、確保関数の確保するストレージは、要求されたサイズ以下の大きさのオブジェクトを配置できるよう、適切にアラインされていなければならない。
単数形の確保関数
void* operator new(std::size_t size) ;
- 効果
-
この確保関数は、new式からsizeバイトのストレージを確保するために呼ばれる。
- 必須の挙動
-
適切にアラインされたストレージを指し示すnullポインターではない値を返す。もしくは、std::bad_exceptionがthrowされる。
- デフォルトの挙動
-
- ループを実行する。ループの中で、まず要求されたストレージの確保を試みる。ストレージ確保の方法は実装依存である。
- ストレージの確保が成功したならば、ストレージへのポインターを返す。ストレージの確保が成功しなかった場合で、現在のnew_handlerがnullポインターの場合、std::bad_allocをthrowする。
- 現在のnew_handlerがnullポインター以外の場合、現在のnew_handlerを呼び出す。呼び出しが返ったならば、ループを続行する。
- ループはストレージの確保が成功するか、new_handlerの呼び出しが返らなくなるまで、続けられる。
#include <cstddef>
#include <cstdlib>
#include <new>
void* operator new( std::size_t size )
{
// std::mallocに実引数0を渡した場合の挙動は定義されていない
if ( size == 0 ) { size = 1 ; }
while ( true ) // ループを実行する
{
// ループの中で、要求されたストレージの確保を試みる
void * ptr = std::malloc( size ) ;
// ストレージの確保が成功したならば
if ( ptr != nullptr )
{ // ストレージへのポインターを返す
return ptr ;
}
// ストレージの確保が成功しなかった場合
std::new_handler handler = std::get_new_handler() ;
if ( handler == nullptr ) // 現在のnew_handlerがnullポインターの場合
{ // std::bad_allocをthrowする。
throw std::bad_alloc() ;
} else // 現在のnew_handlerがnullポインターではない場合
{ // 現在のnew_handlerを呼び出す
handler( ) ;
}
// ループを続行する
}
}
nothrow版の単数形の確保関数
void * operator new( std::size_t size, const std::nothrow_t & ) noexcept ;
- 効果
-
前項の確保関数と同じ。ただし、エラー報告としてstd::bad_allocをthrowする代わりに、nullポインターを返す。
- 必須の挙動
-
適切にアラインされたストレージへのポインターを返すか、nullポインターを返す。この関数の返すポインター、nothrowではない確保関数を呼び出してストレージを確保した場合と同じポインターを返す。
- デフォルトの挙動
-
operator new(size)を呼び出す。呼び出しが通常通り返れば、その戻り値を返す。それ以外の場合、nullポインターを返す。
void* operator new( std::size_t size, const std::nothrow_t & ) noexcept
{
try
{ // operator new(size)を呼び出す
// 呼び出しが通常通り返れば、その戻り値を返す
return operator new( size ) ;
}
catch ( ... )
{ // それ以外の場合、nullポインターを返す。
return nullptr ;
}
}
単数形の解放関数
void operator delete( void * ptr ) noexcept ;
- 効果
-
この解放関数は、ptrの値を無効にするため、delete式から呼ばれる。ptrの値は、nullポインターか、operator new(std::size_t) もしくは operator new(std::size_t,const std::nothrow_t&)によって返された値で、まだoperator delete(void*)に渡していないものである。
- デフォルトの挙動
-
ptrの値がnullポインターであれば、なにもしない。それ以外の場合、先のoperator newの呼び出しで確保されたストレージを解放する。
void operator delete( void* ptr ) noexcept
{
std::free( ptr ) ;
}
nothrow版の単数形の解放関数
void operator delete( void * ptr, const std::nothrow_t & ) noexcept ;
- 効果
-
nothrow版のnew式によって呼び出されたコンストラクターが例外を投げた時に、ストレージを解放するために呼ばれる。delete式では呼ばれない。
struct X
{
X() { throw 0 ; }
} ;
int main()
{
new(std::nothrow) X ; // operator delete( void *, std::nothrow_t &)が呼ばれる
}
- デフォルトの挙動
-
operator delete(ptr)を呼び出す。
void operator delete( void* ptr, const std::nothrow_t & ) noexcept
{
operator delete( ptr ) ;
}
配列形の確保関数
void * operator new[]( std::size_t size ) ;
- 効果
-
この確保関数は配列形のnew式からsizeバイトのストレージを確保するために呼ばれる。
- 必須の挙動
-
単数形の確保関数と同じ。
- デフォルトの挙動
-
operator new(size)を返す。
void * operator new[]( std::size_t size )
{
return operator new( size ) ;
}
nothrow版の配列形の確保関数
void * operator new[]( std::size_t size, const std::nothrow_t & ) noexcept ;
- 効果
-
この確保関数は、nothrow版のnew式から呼ばれる。エラー報告として、std::bad_allocをthrowする代わりに、nullポインターを返す。
- 必須の挙動
-
適切にアラインされたストレージへのポインターを返すか、nullポインターを返す。
- デフォルトの挙動
-
operator new[](size)を呼び出す。呼び出しが通常通り返れば、その結果を返す。それ以外の場合は、nullポインターを返す。
void * operator new[]( std::size_t size, const std::nothrow_t & ) noexcept
{
try
{
return operator new[]( size ) ;
}
catch ( ... )
{
return nullptr ;
}
}
配列型の解放関数
void operator delete[]( void * ptr ) noexcept ;
- 効果
-
この解放関数は、配列型のdelete式から、ptrの値を無効にするために呼ばれる。
- デフォルトの挙動
-
operator delete(ptr)を呼ぶ。
void operator delete[]( void * ptr ) noexcept
{
operator delete( ptr ) ;
}
nothrow版の配列型の解放関数
void operator delete[]( void * ptr, const std::nothrow_t & ) noexcept ;
- 効果
-
nothrow版の配列型のnew式によって呼び出されたコンストラクターが例外を投げた時に、ストレージを解放するために呼ばれる。配列型のdelete式では呼ばれない。
- デフォルトの挙動
-
operator delete[](ptr)を呼び出す。
void operator delete[]( void * ptr, const std::nothrow_t & ) noexcept
{
operator delete[]( ptr ) ;
}
以下の形のオーバーロード演算子は、ユーザー定義リテラル演算子のオーバーロードである。
operator "" 識別子
""と識別子の間には、必ずひとつ以上の空白文字を入れなければならない。また、識別子の先頭文字は、必ずアンダースコアひとつから始まらなければならない。ただし、通常の識別子では、アンダースコアから始まる名前は予約されているので注意すること。これは、ユーザー定義リテラル演算子のみの特別な条件である。
// OK
void operator "" /* 空白文字が必要 */ _x( unsigned long long int ) ;
// エラー、""と_yの間に空白文字がない
void operator ""_y( unsigned long long int ) ;
// エラー、識別子がアンダースコアから始まっていない
void operator "" z( unsigned long long int ) ;
// エラー、""の間に空白文字がある
void operator " " _z( unsigned long long int ) ;
リテラル演算子の仮引数リストは、以下のいずれかでなければならない。
const char*
unsigned long long int
long double
char
wchar_t
char16_t
char32_t
const char*, std::size_t
const wchar_t*, std::size_t
const char16_t*, std::size_t
const char32_t*, std::size_t
上記以外の仮引数リストを指定すると、エラーとなる。
リテラル演算子テンプレートは、仮引数リストが空で、テンプレート仮引数は、char型の非型テンプレート仮引数の仮引数パックでなければならない。
template < char ... Chars >
void operator "" _x () { }
これ以外のテンプレート仮引数を取るリテラル演算子テンプレートはエラーとなる。
リテラル演算子は、Cリンケージを持つことができない。
// エラー
extern "C" void operator "" _x( unsigned long long int ) { }
// OK
extern "C++" void operator "" _x( unsigned long long int ) { }
リテラル演算子は、名前空間スコープで宣言しなければならない。つまり、クラススコープで宣言することはできない。ただし、friend関数になることはできる。
// グローバル名前空間スコープ
void operator "" _x( unsigned long long int ) { }
namespace ns {
// ns名前空間スコープ
void operator "" _x( unsigned long long int ) { }
}
class X
{
// OK、friend宣言できる
friend void operator "" _x( unsigned long long int ) ;
// エラー、クラススコープでは宣言できない
static void operator "" _y( unsigned long long int ) ;
} ;
ただし、名前空間スコープで宣言したリテラル演算子を、ユーザー定義リテラルとして使うには、using宣言かusingディレクティブが必要となる。
namespace ns {
void operator "" _x( unsigned long long int ) { }
}
int main( )
{
1_x ; // エラー、operator "" _xは見つからない
{
using namespace ns ;
1_x ; // OK
}
{
using ns::operator "" _x ;
1_x ; // OK
}
}
これ以外は、通常の関数と何ら変りない。例えば、明示的に呼び出すこともできるし、その際には通常のオーバーロード解決に従う。inlineやconstexpr関数として宣言することもできる。内部リンケージでも外部リンケージのどちらでも持てる。アドレスも取得できる。等々。
例外(Exception)は、実行を例外ハンドラーに移す機能である。例外はスレッドごとに存在する。実行を例外ハンドラーに移す際に、オブジェクトを渡すことができる。例外ハンドラーに実行を移すには、tryブロックの中か、tryブロックの中で呼ばれている関数の中でthrow式を使う。
tryブロック:
try 複合文 ハンドラーseq
関数tryブロック:
try コンストラクター初期化子opt 複合文 ハンドラーseq
ハンドラーseq:
ハンドラー ハンドラーseq
ハンドラー:
catch ( 例外宣言 ) 複合文
throw式:
throw 代入式opt
tryブロック文の文法は、キーワードtryに続いて複合文を書き、ひとつ以上のハンドラーを記述する。throw式の型はvoidである。throw式を実行するコードのことを、「例外を投げる(throw an exception)」コードといい、処理がハンドラーに移る。
int main()
{
try
{
throw 0 ; // int型のオブジェクトをthrowする
}
catch ( int i )
{
// int型のオブジェクトがthrowされた時に実行される
}
catch ( double d )
{
// double型のオブジェクトがthrowされた時に実行される
}
catch ( ... )
{
// 任意の型のオブジェクトがthrowされた時に実行される
}
}
goto文やswitch文を使い、tryブロックやハンドラーの外側から内側に処理を移してはならない。tryブロック内やハンドラー内の移動はできる。
int main()
{
// エラー、tryブロックの外側から内側に処理を移す
goto begin_try_block ;
// エラー、ハンドラーの外側から内側に処理を移す
goto begin_handler ;
try
{
begin_try_block: ;
// OK、tryブロック内の移動
goto end_try_block ;
end_try_block: ;
}
catch ( ... )
{
begin_handler: ;
// OK、ハンドラー内の移動
goto end_handler ;
end_handler: ;
}
}
void f( int i )
{
switch( i )
{
// OK
case 0 : ;
try
{
// エラー
case 1 : ;
}
catch ( ... )
{
// エラー
case 2 : ;
}
// OK
case 4 : ;
}
}
goto文、break文、return文、continue文を使って、tryブロックとハンドラーの内側から外側に抜けることができる。
void f()
{
try
{
goto end_f ; // OK
}
catch ( ... )
{
return ; // OK
}
end_f : ;
}
関数tryブロック(function-try-block)は、関数の本体に記述できる。
void f()
try
{
}
catch ( ... )
{
}
コンストラクターの関数の本体として記述する場合には、tryと複合文の間にコンストラクター初期化子を記述する。
struct X
{
int m1 ;
int m2 ;
X()
try
: m1(0), m2(0) // コンストラクター初期化子
{ }
catch ( ... ) { }
} ;
関数tryブロックがコンストラクターかデストラクターの本体に用いられた場合、複合文と、構築と破棄の際にクラスのサブオブジェクトが例外を投げた場合、ハンドラーに処理が移る。
コンストラクターに関数tryブロックを使う例
// 構築時の例外を投げるクラス
struct throw_at_construction
{
throw_at_construction()
{
throw 0 ;
}
} ;
struct X
{
// 構築時に例外を投げるデータメンバー
throw_at_construction member ;
X()
try : member()
{ }
catch ( ... ) { } // ハンドラーに処理が渡る
} ;
// 構築時に例外を投げる基本クラス
struct Y : throw_at_construction
{
Y()
try { }
catch ( ... ) { } // ハンドラーに処理が渡る
} ;
デストラクターに関数tryブロックを使う例
// 破棄時に例外を投げるクラス
struct throw_at_destruction
{
~throw_at_destruction()
{
throw 0 ;
}
} ;
struct X
{
throw_at_destruction member ;
~X()
try { }
catch ( ... ) { }
} ;
struct Y : throw_at_destruction
{
~Y()
try { }
catch ( ... ) { }
} ;
例外を投げる(throwing an exception)とは、日本語では他にも、送出するとかスローするなどとも書かれている。
例外を投げると、処理はハンドラーに移る。例外を投げるときには、オブジェクトが渡される。オブジェクトの型によって、処理が渡されるハンドラーが決まる。
// int型
throw 0 ;
// const char *型
throw "hello" ;
struct X { } ;
X x ;
// X型
throw x ;
例外が投げられると、型が一致する最も近い場所にあるハンドラーに処理が移る。「最も近い」というのは、最近に入って、まだ抜けていないtryブロックに対応するハンドラーである。
// 例外を投げる
void f() { throw 0 ; }
int main()
{
try
{
try
{
try { f() } // 関数fの中で例外を投げる
catch ( ... ) { } // ここに処理が移る
}
catch ( ... ) { }
}
catch ( ... ) { }
}
throw式はオペランドから一時オブジェクトを初期化する。この一時オブジェクトを例外オブジェクト(exception object)という。例外オブジェクトの型を決定するには、throw式のオペランドの型からトップレベルのCV修飾子を取り除き、T型への配列型はTへのポインター型へ、T型を返す関数型は、T型を返す関数へのポインター型に変換する。
throw 0 ; // int
int const a = 0 ;
throw a ; // int
int const volatile * const volatile p = &a ;
throw p ; // int const volatile *
int b[5] ;
throw b ; // int *
int f( int ) ;
throw f ; // int (*)(int)
この一時オブジェクトはlvalueであり、型が適合するハンドラーの変数の初期化に使われる。
void f()
{
try
{
throw 0 ; // 例外オブジェクトはint型のlvalue
}
catch ( int exception_object ) // 例外オブジェクトで初期化される
{ }
}
例外オブジェクトの型が不完全型か不完全型へのポインター型である場合は、エラーとなる。
struct incomplete_type ;
void f()
{
// エラー、不完全型へのポインター型
throw static_cast<incomplete_type *>(nullptr) ;
}
ただし、void型はその限りではない。
void f()
{
// OK、void *
throw static_cast<void *>(nullptr) ;
}
いくつかの制限を除けば、throw式のオペランドは、関数への実引数やreturn文のオペランドとほぼ同じ扱いになっている。
例外オブジェクトのメモリーは、未規定の方法で確保される。
例外オブジェクトの寿命の決定にはふたつの条件があり、どちらか遅い方に合わせて破棄される。
ひとつは例外を再び投げる以外の方法で、例外を捉えたハンドラーから抜け出すこと。
void f()
{
try
{
throw 0 ;
}
catch ( ... )
{
// return文やgoto文などでハンドラーの複合文の外側に移動するか
// あるいはハンドラーの複合文を最後まで処理が到達すれば、例外オブジェクトは破棄される
}
}
例外が再び投げられた場合は、例外オブジェクトの寿命は延長される。
void f() ; // 例外を投げるかもしれない関数
void g() {
try { f() ; }
catch ( ... )
{
throw ; // 例外を再び投げる
}
}
この場合、例外オブジェクトは破棄されずに、例外処理が続行する。
もうひとつの条件は、例外オブジェクトを参照する最後のstd::exception_ptrが破棄された場合。これはライブラリの話になるので、本書ではstd::exception_ptrについては解説しない。
例外オブジェクトのストレージが解放される方法は未規定である。
例外オブジェクトの型がクラスである場合、クラスのコピーコンストラクターかムーブコンストラクターのどちらか片方と、デストラクターにアクセス可能でなければならない。
以下のようなクラスは、例外オブジェクトとして投げることができる。
// 例外オブジェクトとして投げられるクラス
// コピーコンストラクター、ムーブコンストラクター、デストラクターにアクセス可能
struct throwable1
{
throwable1( throwable1 const & ) { }
throwable1( throwable1 && ) { }
~throwable1() { }
} ;
// 例外オブジェクトとして投げられるクラス
// コピーコンストラクター、デストラクターにアクセス可能
struct throwable2
{
throwable2( throwable2 const & ) { }
throwable2( throwable2 && ) = delete ;
~throwable2() { }
} ;
// 例外オブジェクトとして投げられるクラス
// ムーブコンストラクター、デストラクターにアクセス可能
struct throwable3
{
throwable3( throwable3 const & ) = delete ;
throwable3( throwable3 && ) { }
~throwable3() { }
} ;
例外オブジェクトとして投げられるクラスの条件を満たすには、コピーコンストラクターとムーブコンストラクターは、どちらか片方だけアクセスできればよい。デストラクターには必ずアクセス可能でなければならない。
以下のようなクラスは投げることができない。
// 例外オブジェクトとして投げられないクラス
struct unthrowable
{
// コピーコンストラクター、ムーブコンストラクター両方にアクセスできない
unthrowable( unthrowable const & ) = delete ;
unthrowable( unthrowable && ) = delete ;
// デストラクターにアクセスできない
~unthrowable() = delete ;
} ;
たとえ、コピーやムーブが省略可能な文脈でも、コピーコンストラクターかムーブコンストラクターのどちらか片方にはアクセス可能という条件を満たしていなければ、クラスは例外オブジェクトとして投げることができない。
例外は、あるハンドラーに処理が移った段階で、とらえられた(キャッチされた)とみなされる。ただし、例外がとらえられたハンドラーから再び投げられた場合は、再びとらえられていない状態に戻る。
try
{
throw 0 ;
}
catch ( ... )
{
// 例外はとらえられた
throw ; // 再びとらえられていない状態に戻る
}
例外オブジェクトとして投げられる初期化式の評価が完了した後から、例外がとらえられるまでの間に、別の例外が投げられた場合は、std::terminateが呼ばれる。
これが起こるよくある状況は、スタックアンワインディングの最中にデストラクターから例外が投げられることだ。
// デストラクターが例外を投げるクラス
struct C
{
// デストラクターに明示的な例外指定がない場合、この文脈では暗黙にthrow()になるため
// デストラクターの外に例外を投げるには例外指定が必要
~C() noexcept( false ) { throw 0 ; }
} ;
int main()
{
try
{
C c ;
throw 0 ;
// C型のオブジェクトcが破棄される
// 例外中に例外が投げられたため、std::terminateが呼ばれる
}
catch ( ... ) { }
}
一般的に、デストラクターから例外を投げるべきではない。
初期化式の評価が完了した後という点に注意。throw式のオペランドの初期化式の評価中の例外はこの条件に当てはまらない。
struct X
{
X() { throw 0 ; }
} ;
int main( )
{
try
{
// OK、初期化式の評価中の例外
// 例外オブジェクトの型はint
throw X() ;
}
catch ( X & exception ) { }
catch ( int exception ) { } // このハンドラーでとらえられる
}
この例ではX型のオブジェクトを例外としてthrowする前に、初期化中にint型の例外が投げられたので、結果として投げられる例外オブジェクトの型はint型になる。
ただし、初期化式の評価が完了した後という点に注意。初期化完了の後に例外が投げられた場合は、std::terminateが呼ばれる。
// この例がstd::terminateを呼ぶかどうかは、C++の実装次第である。
struct X
{
X( X const & ) { throw 0 ; }
} ;
int main( )
{
try
{
// 実装がコピーを省略しない場合、std::terminateが呼ばれる
// コピーコンストラクターの実行は評価完了後
throw X() ;
}
catch ( ... ) { }
}
この文脈では、賢いC++の実装ならば、コピーを省略できる。ただし、コピーが省略される保証はない。もし、例外オブジェクトを構築する際にコピーが行われたならば、それはthrow式のオペランドの初期化式の評価完了後なので、この条件に当てはまり、std::terminateが呼ばれる。
また、現行の規格の文面には誤りがあり、以下のコードではstd::terminateが呼ばれるよう解釈できてしまう。
// 例外によって抜け出す関数
void f() { throw 0 ; }
struct C
{
~C()
{
// 例外によって抜け出す関数を呼ぶ
try { f() ; }
catch ( ... ) { }
}
} ;
int main()
{
try
{
C c ;
throw 0 ;
// 例外がハンドラーにとらえられる前に、cのデストラクターが呼ばれる
}
catch ( ... ) { }
}
これは規格の誤りであり、本書執筆の時点で、修正が検討されている。
オペランドのないthrow式は、現在とらえられている例外を再び投げる(rethrow)。これは、再送出とかリスローなどとも呼ばれている。例外が再び有効になり、例外オブジェクトは破棄されずに再利用される。つまり、例外をふたたび投げる際に一時オブジェクトを新たに作ることはない。例外は再びとらえられているものとはみなされなくなり、std::uncaught_exception()の値も、またtrueになる。
int main()
{
try
{
try
{
throw 0 ;
}
catch ( int e )
{ // 例外をとらえる
throw ; // 一度捉えた例外を再び投げる
}
}
catch ( int e )
{
// 再び投げられた例外をとらえる
}
}
例外がとらえられていない状態でオペランドのないthrow式を実行すると、std::terminateが呼ばれる。
int main()
{
throw ; // std::terminateが呼ばれる
}
処理がthrow式からハンドラーに移るにあたって、tryブロックの中で構築された自動オブジェクトのデストラクターが呼び出される。自動オブジェクトの破棄は構築の逆順に行われる。
struct X
{
X() { }
~X() { }
} ;
int main()
{
try
{
X a ;
X b ;
X c ;
// a, b, cの順に構築される
throw 0 ;
}
// このハンドラーに処理が移る過程で、
// c, b, aの順に破棄される
catch ( int ) { }
}
オブジェクトの構築、破棄が、例外により中断された場合、完全に構築されたサブオブジェクトに対してデストラクターが実行される。オブジェクトが構築されたストレージの種類は問わない。
struct Base
{
Base() { }
~Base() { }
} ;
// コンストラクターに実引数trueが渡された場合、例外を投げるクラス
struct Member
{
Member( bool b )
{
if ( b )
throw 0 ;
}
~Member() { }
} ;
// Xのサブオブジェクトは、基本クラスBaseと、非staticデータメンバー、a, b, c
struct X : Base
{
Member a, b, c ;
X() : a(false), b(true), c(false)
{ }
// Base, aのデストラクターが実行される。
~X() { }
} ;
int main()
{
try
{
X x ;
}
catch ( int ) { }
}
この例では、クラスXは、サブオブジェクトとして、Base型の基本クラスと、Member型の非staticデータメンバー、a, b, cを持つ。その初期化順序は、基本クラスBase, a, b, c, Xである。クラスMemberは、コンストラクターの実引数にtrueが渡された場合、例外を投げる。クラスXのコンストラクターは、bのコンストラクターにtrueを与えている。その結果、クラスXのオブジェクトの構築は、例外によって中断される。
この時、デストラクターが実行されるのは、基本クラスBaseのオブジェクトと、Member型の非staticデータメンバーaのオブジェクトである。bは、コンストラクターを例外によって抜けだしたため、構築が完了していない。cは、まだコンストラクターが実行されていないため、構築が完了していない。そのため、b, cのオブジェクトに対してデストラクターは実行されない。
ただし、union風クラスのvariantメンバーには、デストラクターは呼び出されない。
struct Member
{
Member() { }
~Member() { }
} ;
struct X
{
union { Member m ; } ;
X() { throw 0 ; } // mのデストラクターは実行されない
~X() { }
} ;
あるオブジェクトの非デリゲートコンストラクターの実行が完了し、その非デリゲートコンストラクターを呼び出したデリゲートコンストラクターが例外によって抜けだした場合、そのオブジェクトに対してデストラクターが呼ばれる。
struct X
{
// 非デリゲートコンストラクター
X( bool ) { }
// デリゲートコンストラクター
X() : X( true )
{
throw 0 ; // Xのデストラクターが呼ばれる
}
~X() { }
} ;
これは、オブジェクトの構築完了は、非デリゲートコンストラクターの実行が完了した時点だからだ。
例外によって構築が中断されたオブジェクトがnew式によって構築された場合、使われた確保関数に対応する解放関数があれば、ストレージを解放するために自動的に呼ばれる。
struct X
{
X() { throw 0 ; }
~X() { }
// 確保関数
void * operator new( std::size_t size ) noexcept
{
return std::malloc( size ) ;
}
// 上記確保関数に対応する解放関数
void operator delete( void * ptr ) noexcept
{
std::free( ptr ) ;
}
} ;
int main()
{
try
{
new X ; // 対応する解放関数が呼ばれる
}
catch ( int ) { }
}
この例では、Xを構築するためにmallocで確保されたストレージは、正しくfreeで解放される。
throw式から処理を移すハンドラーまでのtryブロック内の自動ストレージ上のオブジェクトのデストラクターを自動的に呼ぶこの一連の過程は、スタックアンワインディング(stack unwinding)と呼ばれている。もし、スタックアンワインディング中に呼ばれたデストラクターが例外によって抜けだした場合、std::terminateが呼ばれる。
struct X
{
X() { }
~X() noexcept(false)
{
throw 0 ;
}
} ;
int main()
{
try
{
X x ;
throw 0 ; // std::terminateが呼ばれる
}
catch ( int ) { }
}
現行の文面を解釈すると、以下のコードもstd::terminateを呼ぶように解釈できるが、これは誤りであり、将来の規格改定で修正されるはずである。
struct Y
{
Y() { }
~Y() noexcept(false) { throw 0 ; }
} ;
struct X
{
X() { }
~X() noexcept(false)
{
try
{
// スタックアンワインディング中に呼ばれたデストラクターが例外によって抜け出す
// 現行の規格の文面解釈ではstd::terminateが呼ばれてしまう
Y y ;
}
catch ( int ) { }
}
} ;
int main()
{
try
{
X x ;
throw 0 ;
}
catch ( int ) { }
}
一般に、デストラクターを例外によって抜け出すようなコードは書くべきではない。デストラクターはスタックアンワインディングのために呼ばれるかもしれないからだ。スタックアンワインディング中かどうかを調べる、std::uncaught_exceptionのような標準ライブラリもあるにはあるが、スタックアンワインディング中かどうかを調べる必要は、通常はない。
C++11からは、デストラクターはデフォルトで例外指定がつくようになり、ほとんどの場合、noexcept(true)と互換性のある例外指定になる変更がなされたのも、通常はデストラクターを例外で抜け出す必要がないし、またそうすべきではないからだ。
throw式によって投げられた例外は、tryブロックのハンドラーによって捕捉される。ハンドラーの文法は以下の通り。
catch ( 例外宣言 ) 複合文
int main()
{
try
{
throw 0 ; // 例外オブジェクトの型はint
}
catch ( double d ) { }
catch ( float f ) { }
catch ( int i ) { } // このハンドラーに処理が移る
}
例外が投げられると、処理は、例外オブジェクトの型と適合(match)する例外宣言を持つハンドラーに移される。
ハンドラーの例外宣言は、不完全型、抽象クラス型、rvalueリファレンス型であってはならない。
struct incomplete ; // 不完全型
struct abstract
{
void f() = 0 ;
} ;
int main()
{
try { }
catch ( incomplete x ) { } // エラー、不完全型
catch ( abstract a ) { } // エラー、抽象クラス型
catch ( abstract * a ) { } // OK、抽象クラスへのポインター型
catch ( abstract & a ) { } // OK、抽象クラスへのリファレンス型
catch ( int && rref) { } // エラー、rvalueリファレンス型
}
また、例外宣言の型は、不完全型へのポインターやリファレンスであってはならない。ただし、void *, const void *, volatile void *, const volatile void *は、不完全型へのポインター型だが、例外的に許可されている。
ハンドラーの例外宣言が「Tへの配列」の場合、「Tへのポインター」型に変換される。「Tを返す関数」型は、「Tを返す関数へのポインター」型に変換される。
catch ( int [5] ) // int *と同じ
catch ( int f( void ) ) // int (*f)(void)と同じ
あるハンドラーが、例外オブジェクトの型Eと適合する条件は以下の通り。
-
ハンドラーの型が cv Tもしくは cv T &で、EとTが同じ型である場合。
cvは任意のCV修飾子(const, volatile)のことで、トップレベルのCV修飾子は無視される。
たとえば、例外オブジェクトの型がintの場合、以下のようなハンドラーが適合する。
catch ( int )
catch ( const int )
catch ( volatile int )
catch ( const volatile int )
catch ( int & )
catch ( const int & )
catch ( volatile int & )
catch ( const volatile int & )
-
ハンドラーの型がcv Tかcv T &で、TはEの曖昧性のないpublicな基本クラスである場合
例えば、以下のような例が適合する。
struct Base { } ;
struct Derived : public Base { } ;
int main()
{
try
{
Derived d ;
throw d ; // 例外オブジェクトの型はDerived
}
catch ( Base & ) { } // 適合、BaseはDerivedの曖昧性のないpublicな基本クラス
}
以下のような例は適合しない。
struct Base { } ;
struct Ambiguous { } ;
struct Derived : private Base, public Ambiguous { } ;
struct Sub : public Derived, public Ambiguous { } ;
int main()
{
try
{
Sub sub ;
throw sub ; // 例外オブジェクトの型はSub
}
catch ( Base & ) { } // 適合しない、非public基本クラス
catch ( Ambiguous & ) { } // 適合しない、曖昧
}
-
ハンドラーの型がcv1 T* cv2で、Eがポインター型で、以下のいずれかの方法でハンドラーの型に変換可能な場合
-
標準ポインター型変換で、privateやprotectedなポインターへの変換や、曖昧なクラスへの変換を伴わないもの
struct Base { } ;
struct Derived : public Base { } ;
int main()
{
try
{
Derived d ;
throw &d ; // 例外オブジェクトの型はDerived
}
catch ( Base * ) { } // 適合、BaseはDerivedの曖昧性のないpublicな基本クラス
}
-
修飾変換
int main()
{
int i ;
try
{
throw &i ;
}
catch ( const int * ) { }
}
-
ハンドラーの型がポインターかメンバーへのポインターで、Eがstd::nullptr_tの場合
struct X
{
int member ;
} ;
int main()
{
try
{
throw nullptr ;
}
catch ( void * ) { } // 適合
catch ( int * ) { } // 適合
catch ( X * ) { } // 適合
catch ( int X::* ) { } // 適合
}
nullptrの型であるstd::nullptr_t型の例外オブジェクトは、あらゆるポインター型、メンバーへのポインター型に適合する。
throw式のオペランドが定数式で0と評価される場合でも、ポインターやメンバーへのポインター型のハンドラーには適合しない。
int main()
{
try
{
throw 0 ; // 例外オブジェクトの型はint
}
catch ( int * ) { } // 適合しない
}
tryブロックのハンドラーは、書かれている順番に比較される。
int main()
{
try
{
throw 0 ; // 例外オブジェクトの型はint
}
catch ( int ) { } // 適合する。処理はこのハンドラーに移る
catch ( const int ) { }
catch ( int & ) { }
}
この例では、3つのハンドラーはどれも例外オブジェクトの型に適合するが、比較は書かれている順番に行われる。一番初めに適合したハンドラーに処理が移る。関数のオーバーロード解決のような、ハンドラー同士の型の適合の優劣の比較は行われない。
ハンドラーの例外宣言に...が使われた場合、そのハンドラーはどの例外にも適合する。
void f()
{
try { }
catch ( int ) { }
catch ( double ) { }
catch ( ... ) { } // どの例外にも適合する
}
...ハンドラーを使う場合は、tryブロックのハンドラーの最後に記述しなければならない。
void f()
{
try { }
catch ( ... ) { }
catch ( int ) { } // エラー
}
tryブロックのハンドラーのうちに、適合するハンドラーが見つからない場合、同じスレッド内で、そのtryブロックのひとつ上のtryブロックが試みられる。
void f()
{
try { throw 0 ; } // 例外オブジェクトの型はint
catch ( double ) { } // 適合しない
}
void g()
{
try
{
f() ;
}
catch ( int ) { } // 適合する
}
int main()
{
try
{
g() ;
}
catch ( ... ) { }
}
catch句の仮引数の初期化が完了した時点で、ハンドラーはアクティブ(active)になったとみなされる。スタックはこの時点でアンワインドされている。例外を投げた結果、std::terminateやstd::unexpectedが呼ばれた場合、暗黙のハンドラーというものがアクティブになったものとみなされる。catch句から抜けだした場合、ハンドラーはアクティブではなくなる。
現在、アクティブなハンドラーが存在する場合、直前に投げられた例外を、現在捕捉されている例外(currently handled exception)と呼ぶ。
適合するハンドラーが見つからない場合、std::terminateが呼ばれる。std::terminateが呼ばれる際、スタックがアンワインドされるかどうかは実装次第である。
コンストラクターとデストラクターの関数tryブロックのハンドラー内で、非staticデータメンバーかオブジェクトの基本クラスを参照した場合、挙動は未定義である。
struct S
{
int member ;
S()
try
{
throw 0 ;
}
catch ( ... )
{
int x = member ; // 挙動は未定義
}
} ;
コンストラクターの関数tryブロックのハンドラーに処理が移る前に、完全に構築された基本クラスと非staticメンバーのオブジェクトは、破棄される。
struct Base
{
Base() { }
~Base() { }
} ;
struct Derived : Base
{
Derived()
try
{
throw 0 ;
}
catch ( ... )
{
// 基本クラスBaseのオブジェクトはすでに破棄されている
// 非staticデータメンバーのオブジェクトについても同様
}
} ;
オブジェクトの非デリゲートコンストラクターの実行が完了したあとに、デリゲートコンストラクターが例外を投げた場合は、オブジェクトのデストラクターが実行されたあとに、関数tryブロックのハンドラーに処理が移る。
struct S
{
// 非デリゲートコンストラクター
S() { }
// デリゲートコンストラクター
S( int ) try
: S()
{ throw 0 ; }
catch ( ... )
{
// デストラクターS::~Sはすでに実行されている
}
~S() { }
} ;
int main()
{
S s(0) ;
}
非デリゲートコンストラクターの実行完了をもって、オブジェクトは構築されている。デリゲートコンストラクターが例外を投げた場合の関数tryブロックのハンドラーに処理が移る前に、オブジェクトを破棄されなければならない。そのために、ハンドラーに処理が移る前にデストラクターが呼び出されることになる。
デストラクターの関数tryブロックのハンドラーに処理が移る前に、オブジェクトの基本クラスと非variantメンバーは破棄される。
struct Base
{
Base() { }
~Base() { }
} ;
struct Derived : Base
{
~Derived() noexcept(false)
try { throw 0 ; }
catch ( ... )
{
// 基本クラスはすでに破棄されている
// 非staticデータメンバーについても同様
}
} ;
関数のコンストラクターの仮引数のスコープと寿命は、関数tryブロックのハンドラー内まで延長される。
void f( int param )
try
{
throw 0 ;
}
catch ( ... )
{
int x = param ; // OK、延長される
}
静的ストレージ上のオブジェクトのデストラクターから投げられる例外が、main関数の関数tryブロックのハンドラーで捕捉されることはない。threadストレージ上のオブジェクトのデストラクターから投げられる例外が、スレッドの初期関数の関数tryブロックのハンドラーで捕捉されることはない。
コンストラクターの関数tryブロックのハンドラーの中にreturn文がある場合、エラーとなる。
struct S
{
S()
try { }
catch ( ... )
{
return ; // エラー
}
} ;
コンストラクターとデストラクターの関数tryブロックで、処理がハンドラーの終わりに達したときは、現在ハンドルされている例外が、再びthrowされる。
struct S
{
S()
try
{
throw 0 ;
}
catch ( int )
{
// 例外が再びthrowされる
}
} ;
コンストラクターとデストラクター以外の関数の関数tryブロック、処理がハンドラーの終わりに達したときは、関数からreturnする。このreturnは、オペランドなしのreturn文と同等になる。
void f()
try
{
throw 0 ;
}
catch ( int )
{
// return ;と同等
}
もしこの場合に、関数が戻り値を返す関数の場合、挙動は未定義である。
int f()
try
{
throw 0 ;
}
catch ( ... )
{
// 挙動は未定義
}
例外宣言が例外の型と名前を指定する場合、例外の型のオブジェクトがその名前で、例外オブジェクトからコピー初期化される。
int main()
{
try
{
throw 123 ; // 例外オブジェクの型はint、値は123
}
catch ( int e )
{
// eの型はint、値は123
}
}
例外宣言が、例外の型のみで名前を指定していない場合、例外の型の一時オブジェクトが生成され、例外オブジェクトからコピー初期化される。
int main()
{
try
{
throw 123 ;
}
catch ( int )
{
// int型の一時オブジェクトが生成され、例外オブジェクトからコピー初期化される
// 名前がないので、参照する方法はない
}
}
例外宣言の名前の指し示すオブジェクト、あるいは無名の一時オブジェクトの寿命は、処理がハンドラーから抜けだして、ハンドラー内で初期化された一時オブジェクトが解放された後である。
struct S
{
int * p ;
S( int * p ) : p(p) { }
~S() { *p = 0 ; }
} ;
int main()
{
try
{
throw 123 ;
}
catch ( int e )
{
S s( &e ) ;
// sが破棄された後に、eが破棄される
}
}
そのため、上のコードは問題なく動作する。なぜならば、eが破棄されるのはsよりも後だからだ。
ハンドラーの例外宣言が、非constな型のオブジェクトの場合、ハンドラー内でそのオブジェクトに対する変更は、throw式によって生成された一時的な例外オブジェクトには影響しない。
int main()
{
try
{
try
{
throw 0 ;
}
catch ( int e )
{
++e ; // 変更
throw ; // 例外オブジェクトの再throw
}
}
catch ( int e )
{
// eは0
}
}
ハンドラーの例外宣言が、非constな型へのリファレンス型のオブジェクトの場合、ハンドラー内でそのオブジェクトに対する変更は、throw式によって生成された一時的な例外オブジェクトを変更する。この副作用は、ハンドラー内で再throwされたときにも効果を持つ。
int main()
{
try
{
try
{
throw 0 ;
}
catch ( int & e )
{
++e ; // 変更
throw ; // 例外オブジェクトの再throw
}
}
catch ( int e )
{
// eは1
}
}
例外指定(Exception specification)とは、関数宣言で、関数が例外を投げるかどうかを指定する機能である。
関数宣言における例外指定の文法は、リファレンス修飾子の後、アトリビュートの前に記述する。
T D( 仮引数宣言 ) cv修飾子 リファレンス修飾子 例外指定 アトリビュート指定子
例外指定:
noexcept( 定数式 )
noexcept
void f() noexcept ;
struct S
{
void f() const & noexcept [[ ]] ;
} ;
例外指定は、関数宣言と定義のうち、関数型、関数へのポインター型、関数型へのリファレンス、メンバー関数へのポインター型に適用できる。また、関数へのポインター型が仮引数や戻り値の型に使われる場合も指定できる。
void f() noexcept ; // OK
void (*fp)() noexcept = &f ; // OK
void (&fr)() noexcept = f ; // OK
// OK、仮引数として
void g( void (*fp)() noexcept ) ;
// OK、戻り値の型として
auto h() -> void (*)() noexcept ;
struct S
{
void f() noexcept ; // OK
} ;
typedef宣言とエイリアス宣言には使用できない。
typedef void (*func_ptr_type)() noexcept ; // エラー
using type = void (*)() noexcept ; // エラー
例外指定のない関数宣言は、例外を許可する関数である。
例外指定にnoexceptが指定された場合、その関数は例外を許可しないと指定したことになる。
例外指定に、noexcept(定数式)を指定し、定数式がtrueと評価される場合、その関数は例外を許可しないと指定したことになる。定数式がfalseと評価される場合、その関数は例外を許可する関数と指定したことになる。
void f1() ; // 例外を許可
void f2() noexcept ; // 例外を許可しない
void f3() noexcept( true ) ; // 例外を許可しない
void f4() noexcept( false ) ; // 例外を許可
noexcept(定数式)は、コンパイル時の条件に従って、関数の例外指定を変えることに使える。
template < typename T >
constexpr bool is_nothrow()
{
return std::is_fundamental<T>::value ;
}
// テンプレート仮引数が基本型なら例外を投げない実装ができる関数
template < typename T >
void f( T x ) noexcept( is_nothrow<T>() ) ;
この例では、関数fは、テンプレート仮引数が基本型の場合、例外を投げない実装ができるものとする。そこで、テンプレートのインスタンス化の際に、型を調べることによって、例外を許可するかどうかをコンパイル時に切り替えることができる。
もし、例外を許可しない関数が、例外のthrowによって抜け出した場合、std::terminateが呼ばれる。
// 例外を許可する関数
void allow_exception()
{
throw 0 ; // OK
}
// 例外を許可しない関数
void disallow_exception() noexcept
{
try
{
throw 0 ; // OK、例外は関数の外に抜けない
}
catch ( int ) { }
throw 0 ; // 実行時にstd::terminateが呼ばれる
}
例外を許可しないというのは、例外によって関数から抜け出すことを禁止するものであり、関数の中で例外を使うことを禁止するものではない。
例外を許可しない関数は、例外を投げる可能性があったとしても、違法ではない。C++実装は、そのようなコードを合法にするように明確に義務付けられている。
void f() noexcept
{
throw 0 ; // OK、コンパイルが通る
// 実行時にstd::terminateが呼ばれる
}
void g( bool b ) noexcept
{
if ( b )
throw 0 ; // OK、コンパイルが通る
// 実行時にbがtrueの場合、std::terminateが呼ばれる
}
もちろん、そのような関数を呼び出して、結果として関数の外に例外が投げられた場合、std::terminateが呼ばれる。
この他に、C++11では非推奨(deprecated)扱いになっている機能に、動的例外指定(dynamic-exception-specification)がある。この機能は将来廃止されるので、詳しく解説しないが、概ね以下のような機能となっている。
// 例外を許可しない
void f() throw( ) ;
// int型のthrowを許可する
void g() throw( int ) ;
// int型とshort型のthrowを許可する
void h() throw( int, short ) ;
動的例外指定のある関数では、例外を関数の外にthrowすると、std::unexpectedが呼ばれる。もし、許可した型の例外をthrowした場合は、そのままハンドラーの検索が行われるが、許可しない型をthrowした場合は、std::terminateが呼ばれるとされている。
少なくとも、当初のC++の設計はそうであったが、現実には、そのように実装するC++実装は出てこなかった。ほとんどの実装では、動的例外指定は、単に無視された。
その後、何も例外として許可する型を指定しない、throw()だけが、本来の設計とは違う意味で使われだした。関数が外に例外を投げない保証を記述するために使われだしたのだ。この、例外を関数の外に投げない保証というのは、とても便利だったので、C++11では専用の文法を与えられ、無例外指定として追加された。そして、動的例外指定は、現実に実装されていないことから、非推奨に変更された。将来的には取り除かれる予定だ。
クラスの暗黙に宣言される特別なメンバー関数は、この動的例外指定を暗黙に指定される。その型リストは、暗黙の実装が呼び出す関数が投げる可能性のある例外のみを持つ。
これは、基本クラスや非staticメンバーが、明示的に例外を許可するものでないかぎり、クラスの暗黙の特別なメンバーは、無例外指定されるということである。
class S
{
// 暗黙のコンストラクター、デストラクター、代入演算子は、
// 例外指定throw()が指定される
} ;
解放関数の宣言に、明示的な例外指定がない場合は、noexcept(true)が指定されたものとみなされる。
// 暗黙にnoexcept(true)が指定される
void operator delete( void * ) ;