C++
melangの各noteで他言語と比較した、C++の設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
伝統的に型を明示するスタイルが強い。
int age = 20;int age = 20;
auto name = std::string("Alice"); // auto による型推論`auto`によるローカル変数の型推論はC++11以降で使えるが、関数シグネチャなどでは型を明示するのが伝統的なスタイル。テンプレートによる静的なジェネリックプログラミングも特徴。
型注釈の位置
型が変数名の前に来る伝統的なprefix方式。trailing return typeという後置構文も選べる。
int age = 20;int age = 20; // 型が名前より前
auto name = std::string("Alice"); // autoで推論
auto add(int a, int b) -> int { // trailing return typeで戻り値を後置
return a + b;
}基本はCから受け継いだ`Type name`という前置構文。一方C++11で導入されたtrailing return type構文(`auto f() -> Type`)は、テンプレートで戻り値の型を引数から導出する必要から生まれた、後置構文への部分的な移行。同じ言語の中でprefix/postfixが混在する例。
型変換
キャスト構文で変換する。数値の縮小変換は検出されない。
long y = static_cast<long>(x);int x = 10;
long y = static_cast<long>(x);
short s = static_cast<short>(70000); // 黙って値が変わる
int n = std::stoi("42"); // 失敗すると例外
int m = std::stoi("abc"); // std::invalid_argument
// 例外を使わない場合
int v;
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
if (ec != std::errc()) { /* 失敗 */ }`static_cast`や`reinterpret_cast`など、変換の性質ごとに分かれたキャスト構文を持つ。数値のキャストは失敗を検出しない。文字列からのパースは`std::stoi`が例外、`std::from_chars`がエラーコードで失敗を表し、方式が混在している。
暗黙の型変換
算術型の標準変換が広く暗黙に行われる。
int x = 10;int x = 10;
long y = x; // 暗黙変換でOK
double z = x; // int -> double も暗黙変換される
short s = x; // 縮小変換も警告のみで通ることが多い算術型間の標準変換(整数昇格・浮動小数点変換など)が広く暗黙に行われる。縮小変換(intからshortなど)も多くの場合は警告止まりでコンパイルが通り、意図しない情報欠落が起きやすい。ユーザー定義型でも変換コンストラクタやconversion operatorにより暗黙変換を定義できる(`explicit`で抑制可能)。
Enum / Sum Type
enumは整数値の列挙にとどまり、Sum Typeは別に組む。
enum class Shape { Circle, Rectangle };enum class Shape { Circle, Rectangle };
std::variant<double, std::pair<double, double>> payload; // payloadは別途std::variantで表現`enum` / `enum class`は整数値の列挙にとどまり、variantごとに異なるデータを持てない。payload付きの合算型は`std::variant`やタグ付き構造体を自前で組む必要があり、`switch`の網羅性チェックも基本的にない(コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。の警告に頼る程度)。
値の不変性
既定は変更可能。`const`で制限できるが、回避する手段も残る。
const T x; / const T& xconst std::vector<int> v = {1, 2};
// v.push_back(3); // コンパイルエラー
std::vector<int> w = {1, 2};
w.push_back(3); // OK
w = {4}; // OK: 再代入
// constの参照からでも、const_castで外せてしまう
auto& m = const_cast<std::vector<int>&>(v);既定は変更可能で、不変にするのは`const`を付ける側の任意である。`const`は参照やメンバ関数にも付けられ表現力が高いが、`const_cast`や`mutable`メンバで抜け道が残る。不変を保つには書き手の規律が要る。
名前付き引数
名前付き引数はなく、C++20の指定初期化子は集成体の初期化にのみ使える。
void foo(int width, int height);void foo(int width, int height) {}
foo(10, 20); // 引数名は呼び出し側に現れない
struct FooArgs { int width; int height; };
void foo2(FooArgs args) {}
foo2({.width = 10, .height = 20}); // C++20の指定初期化子(集成体のみ)関数呼び出しに名前付き引数の構文はない。C++20で追加された指定初期化子(designated initializers)は集成体の初期化にのみ使え、関数呼び出しの引数を名前で指定することはできない。
関数の引数
値・参照・右辺値参照を選べるが、参照渡しは呼び出し側から見えない。
void f(T x) / void f(const T& x) / void f(T& x) / void f(T&& x)void a(std::string s); // コピー
void b(const std::string& s); // 読み取り専用の参照
void c(std::string& s); // 変更可能な参照
void d(std::string&& s); // 右辺値参照(moveの受け口)
std::string s = "a";
a(s); // 呼び出し側の見た目は
b(s); // どれも同じで、
c(s); // 関数の宣言を見ないと違いが分からない
d(std::move(s));渡し方を最も細かく選べる一方、`a(s)`と`c(s)`は呼び出し側では同じ見た目になる。変数が書き換えられるかは宣言を確認するまで分からず、書き換えの可能性が呼び出し側から見えないという問題は、値を不変にして代入を禁止するmelangでは、そもそも起きない。
if 文/式
`if`はstatement。値の分岐は三項演算子`?:`で書く。
if (cond) { ... } else { ... } / cond ? a : bint x = cond ? 1 : 2;
if (cond) {
process();
}`if`はstatementで、値の分岐は三項演算子を使う。`if constexpr`はコンパイル時の分岐であり、値を返す式ではない。
I/O の明示
副作用を示す標準的な仕組みがない。
std::string readFile(const std::string& path);std::string readFile(const std::string& path) {
std::ifstream file(path);
return std::string((std::istreambuf_iterator<char>(file)), {});
}
int square(int x) {
readFile("x"); // 呼べてしまう
return x * x;
}`noexcept`は例外を投げないことを示すが、I/Oを行うかどうかとは無関係。標準ライブラリのI/O関数とそれ以外の関数は、シグネチャ上まったく区別されない。
言語名
記号を含む言語名で、ファイル名・URL・識別子に使えない。
`+`という記号を含むため、そのままではファイル名・パッケージ名・多くの識別子規則に使えない。`cpp`という代替表記が広く使われ、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。も`g++`・`clang++`のように独自の命名になっている。記号を含む名前は視認性やインパクトはあるが、機械的な取り扱いの面で常に読み替えが必要になる。