最近、C++ を少し真面目に勉強し始めました。
もう5年以上 C++ を書いていますが、自分がいかに無知だったかを思い知らされました。
ということで、自分が知って驚いた C++ の仕様をまとめます。
C++11 現在の仕様(たぶん)なので、C++14 とか C++17 では変わっている可能性もあります。
戻り値を返さなくても良い関数
戻り値がある関数は、必ず値を返さなければなりません。
しかし、1つだけ例外的に戻り値を返さなくても良い関数があります。
main 関数です。
main 関数の戻り値は int ですが、return 文を書かなくてもコンパイルは通ります。
return 文を省略した場合、return 0;
と見なされます。
配列の添え字
一般的に、配列の添え字は a[1]
のように [] の中に配列要素のインデックスを指定します。
これで指定した要素にアクセスできます。
int a[3] = { 0, 1, 2 };
int b = a[1];
しかし、x[y] の x が配列のポインター、y がインデックスという規格はありません。
規格上は、x か y の、どちらか一方が配列のポインターで、もう一方がインデックスであれば良いのです。
つまり a[1]
ではなく 1[a]
と書いてもコンパイルは通ります。
1[a]
という書き方は一般的じゃないし、直感的に意味が分かりにくいので、もし見かけたら文句を言うと思いますけど、規格上は問題ありません。
char != signed char
int == signed int
です。
short == signed short
です。
long == signed long
です。
が、char == signed char
ではありません。
char == unsigned char
でもありません。
char が signed になるか、unsigned になるかは実装依存です(一般的には signed だと思いますが)。
signed で実装されているからと言っても、char == signed char
になるわけではありません。
char
、signed char
、unsigned char
はそれぞれ別物です。
以下はコンパイルエラーになります。
なぜなら short
と signed short
は同じ型なので「toInt
の再定義」になるからです。
int toInt( short s ) {
return s;
}
int toInt( signed short s ) {
return s;
}
以下はコンパイルが通ります。
char
も signed char
も unsigned char
も別の型なので再定義にはなりません。
int toInt( char c ) {
return c;
}
int toInt( signed char c ) {
return c;
}
int toInt( unsigned char c ) {
return c;
}
整数プロモーション
char
と short
は、演算時に自動的にプロモーションが行われます。
プロモーションとは、int
よりもサイズの小さい型から int
への変換のことです(bool
は除く)。
int
以上の型からそれ以上に型への変換、サイズの大きい型から小さい型への変換は「型変換」と呼ぶらしいです。
なお、unsigned char
や unsigned short
の場合は unsigned int
に変換(プロモーション)されます。
以下の b の型は int になります。
short a = 0;
auto b = a + a;
また、
char a = 0;
printf("%lu\n", sizeof(a));
printf("%lu\n", sizeof(-a));
printf("%lu\n", sizeof(a + a));
の結果は
1
4
4
になります。
関数の実引数の評価順
関数の実引数に式を指定する場合、どの順番で評価されるかは実装依存です。
もちろん、関数が呼ばれる前に全ての式が評価されることは保証されています。
以下を実行した結果、
clang 4.0 では 123123123
でしたが
gcc 6.3 と msvc++ 2015 では 321321321
でした。
clang は実引数の先頭から、gcc と msvc++ は末尾から評価しているようです。
std::string& func1( std::string& s ) {
s += "1";
return s;
}
std::string& func2( std::string& s ) {
s += "2";
return s;
}
std::string& func3( std::string& s ) {
s += "3";
return s;
}
void outputString( std::string& s1, std::string& s2, std::string& s3 ) {
std::cout << s1 << s2 << s3 << std::endl;
}
int main(void){
std::string s = "";
outputString( func1( s ), func2( s ), func3( s ) );
}
[おまけ] free や delete は NULL チェック不要
これは2年ぐらい前まで知りませんでした。
意外と知らない人が多そう。
delete や free は NULL を渡しても問題ありません。
よく
if ( p ) {
delete p;
}
// or
if ( p ) {
free( p );
}
という実装を見かけますが、NULLチェックは不要です。
以下の p が nullptr や NULL でも何も起きません(クラッシュしません)。
delete p;
// or
free( p );
私もそうでしたが、なんとなく危険な感じがして、盲目的に NULL チェックしている人が多いのではないでしょうか。
あとがき
「知らなかった」は言い換えると「知らなくても困らなかった」とも言えます。
運良く(?)、それを知らないと困るような仕事をしてこなかったとも言えます。
配列の添字とかは知らなくても何も困らないと思いますが、char の話とか整数プロモーションの話は知っておいた方が良さそうですね。
意外と知らない人も多いと思うので、周りのC++プログラマーに話したらドヤれるかもしれませんw