問題提起
以下のコードを見て、どの関数が呼ばれるか考えてみましょう。
#include <iostream>
#include <vector>
enum Color { Red = 1 };
void display(std::vector<int> vec) {
std::cout << "Vector function called" << std::endl;
}
void display(Color c) {
std::cout << "Enum function called: " << c << std::endl;
}
int main() {
display({});
return 0;
}
多くの人は、{} は空のベクターを表すので、display(std::vector<int>) が呼ばれると考えるでしょう。
しかし、実際に実行すると、出力は「Enum function called: 0」となります。
分析
この挙動を理解するために、まず列挙型の初期化を確認しましょう:
Color c = {};
std::cout << "Color value: " << c << std::endl;
このコードはコンパイル可能で、「Color value: 0」と出力します。これは、列挙型が {} で初期化できることを示しています。
実は、C++ではほとんどの基本型が {} で初期化可能です。コンパイラは、std::vector<int> を初期化するより Color を初期化する方が「より直接的な一致」だと判断し、display(Color) を選択します。
もし display(Color) をコメントアウトし、代わりに display(int) を追加すると:
void display(int num) {
std::cout << "Integer function called: " << num << std::endl;
}
今度は display({}) で「Integer function called: 0」と出力されます。
初期化方法の比較
この問題は、C++の異なる初期化方法の挙動に関連しています:
double value1 = 3.14;
int num1 = value1; // OK: 暗黙の変換
int num2 = {value1}; // エラー: 縮小変換は禁止
int num3(value1); // OK: 暗黙の変換
リスト初期化({}を使用)は縮小変換を禁止するため、より安全です。しかし、C言語では挙動が異なります:
double value = 3.14;
int num1 = value; // OK
int num2 = {value}; // OK
int num3(value); // エラー
C言語ではリスト初期化がサポートされ、複合リテラルとしても使用できます。この違いは、C++がCから継承した特性と、C++が追加した安全性のバランスを反映しています。
実践的な例
この挙動は、より複雑なコードで予期せぬバグを引き起こす可能性があります:
#include <iostream>
#include <string>
#include <vector>
enum class Status { Success = 0, Error = 1 };
void process(std::vector<std::string> data) {
std::cout << "Processing vector with " << data.size() << " elements" << std::endl;
}
void process(Status s) {
std::cout << "Processing status: " << static_cast<int>(s) << std::endl;
}
int main() {
// 意図は空のベクターを渡すこと
process({});
// 明示的にベクターを渡す場合
process(std::vector<std::string>{});
// 明示的にステータスを渡す場合
process(Status{});
return 0;
}
このコードでは、process({}) は process(Status) を呼び出しますが、process(std::vector<std::string>{}) は明示的にベクターバージョンを呼び出します。