C++の
Hello Worldを考える
(オブジェクト指向
と演算子オーバーロード)

  • C++のHello Worldでよく使われるcout <<は、ビットシフト演算子を関数として再定義した演算子オーバーロードの仕組みで動いている。
  • 入門書のcout <<は名前空間・ストリーム・演算子オーバーロードを一行に詰め込んでいるため、初学者だと概念を誤解やすく「クセのある書き方」と認識しにくい。
  • C++23で導入されたstd::printlnを使えば、こうした複雑さを意識せず出力を書ける。

関連記事

1. もう一つのC++のHello World

プログラミングを学ぶとき、最初に書くコードはほぼ決まっています。
Hello Worldです1

C++のHello Worldを考える 従来の書き方 ■ 1985年 Stroustrup 由来 #include <iostream> cout << “Hello” << endl; ← 演算子オーバーロード C++23の書き方 ■ 2023年 標準導入 import std; std::println(“Hello”); ← シンプル・直感的 Hello World はその言語の設計思想を映す

C++23では、Hello Worldプログラムは、このように書けます。

import std;

int main() {
    std::println("Hello, World!");
}Code language: C++ (cpp)

ところが、一般的なC++の入門書では次のように書かれます。

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
}Code language: C++ (cpp)

これは、Stroustrupが1985年に出版した「The C++ Programming Language」に由来する説明です。
このコードは、オブジェクトとメソッドだけでは読めません。

C++のcout <<は何をしているのか、初めて見たときには見当がつきにくいです。

2. <<の正体

cout << "Hello"<<は、演算子オーバーロードで定義された関数呼び出しです。

« の正体:演算子オーバーロード C言語では x << 3 → ビットシフト C++では再定義 operator<< 関数 出力に使用 cout << 演算子表記 cout << “Hello” << ” World” 関数表記 operator<<(operator<<(cout, “Hello”), ” World”) 連鎖できる理由 operator<< が std::ostream& を返すため、戻り値に再び << を呼べる ビットシフト記号がストリーム出力の記法になった

もともとのC言語では、<<はビットシフト演算子でした。

int x = 1;
int y = x << 3;  // 1 を左に3ビットシフト → 8Code language: C++ (cpp)

それを、C++では関数として再定義しています。
operator<<という名前の関数がiostreamの中に定義されており、<<と書くとその関数が呼ばれるのです。

通常の関数呼び出しの形に書き直すと、こうなります。

std::operator<<(std::cout, "Hello, World!");
std::operator<<(std::cout, std::endl);Code language: C++ (cpp)

std::coutはオブジェクト、std::operator<<は関数です。
iostreamをインクルードすることで、この両方が使えるようになります。

また、cout << "Hello" << " World" のように連鎖できるのは、operator<<std::ostream&を返すからです2
戻り値のストリームに対して再びoperator<<を呼べるので、構造としてはネストした関数呼び出しになっています。

operator<<(operator<<(cout, "Hello"), " World");Code language: C++ (cpp)

ストリームという抽象概念を演算子で表現する、C++の特徴的な設計です。

2.1. Stroustrupのストリーム設計

C++は1980年代に、Cとの互換性を強く意識して設計されました。

Stroustrupは、1984年に標準入出力のstdioに変わるライブラリとして、iostreamを実装しました3
このとき、入出力を「ストリーム」という概念で設計し直し、<<という演算子でそれを表現しました。

この設計の利点は、coutofstream(ファイル出力)もstringstream(文字列バッファ)も、同じ<<インターフェースで扱えることです。
operator<<を追加で定義するだけで、自作の型を出力できるようにもなります。
<<という記号が選ばれた背景には、Bell LabsのDoug McIlroyの提案があります4

ターミナルシェルでのリダイレクトに似ているよね。

3. 概念が詰まっている

ただし初学者にとっては、最初の一行に複数の概念が詰まりすぎています。

設計の意図と初学者コスト 設計の利点(1984年) cout 標準出力 ofstream ファイル出力 stringstream 文字列バッファ すべて同じ << で統一 初学者の負荷 名前空間 std:: 演算子オーバーロード using namespace std は実務 NG endl はバッファフラッシュも実行 柔軟な設計の代償として、入門のハードルが上がった

名前空間、ストリームオブジェクト、演算子オーバーロード、テンプレートベースの設計。
入門書でよく見るusing namespace std;も、実務のコードでは名前衝突を招くとして推奨されない書き方です5
こうした事情が重なって、Hello Worldが何をしているのか見えにくくなっています。

また、endlは改行だけでなくバッファのフラッシュも行うため、'\n'より処理が重くなります6

C++のエッセンスがつまったコードですが、入門としては実務に向かないクセを覚えてしまうという側面もあります。

3.1. 素直なオブジェクト指向で書き直すなら

operator<<の呼び出しを明示した形でConsoleクラスを作ると、動作原理がそのまま読めるコードになります。

#include <iostream>

class Console {
public:
    void println(const char* s) {
        std::operator<<(std::cout, s);
        std::operator<<(std::cout, std::endl);
    }
};

int main() {
    Console out;
    out.println("Hello, World!");
}Code language: C++ (cpp)

iostreamの中にcoutというオブジェクトとoperator<<という関数があること、Consoleクラスはそれをラップしていること、mainの中でオブジェクトを作ってメソッドを呼ぶこと。
この三つがそのまま読めます。

少しぎこちない書き方ではありますが、最初の一行で演算子オーバーロードとストリームを同時に学ぶよりは、概念の負荷が小さくなります。

3.2. C++23のstd:println

とはいえ、coutはクセが強いです。

C++23ではstd::printlnが導入され、cout <<を使わずに書けるようになりました7

import std;

int main() {
    std::println("Hello, World!");
}Code language: C++ (cpp)

Pythonのprint関数に近い使い方になり、現代的になりました。

4. Hello Worldが教えること

Hello Worldは単なる動作確認ではなく、その言語の設計思想が凝縮されています。
何を重視した言語かが、Hello Worldの時点で既に現れています。

C++は柔軟な表現力を優先し、演算子の再定義まで含めた設計を標準に採用しました。
C++のHello Worldが少しわかりにくいのは、C++が「そういう柔軟な言語」だからです。

  1. Hello Worldというプログラム例はBrian Kernighanが1972年にBell Labsで言語Bのチュートリアルとして書いたのが起源です。1974年の社内文書「Programming in C: A Tutorial」を経て、1978年にKernighanとDennis Ritchieが出版した「The C Programming Language」で広く知られるようになりました。 – Wikipedia: “Hello, World!” program
  2. 戻り値として同じストリームオブジェクトへの参照を返すことで、operator<<(operator<<(cout, "Hello"), " World")のようにネストした呼び出しが成立します。これはメソッドチェーンと同じ原理です。 – cppreference: std::basic_ostream::operator<<
  3. Bjarne Stroustrupは1984年にC標準ライブラリの型安全な代替として最初のストリームI/Oライブラリを実装しました。その後テンプレート化や名前空間への移動など改良が重ねられ、1998年の標準化でヘッダが<iostream.h>から<iostream>に変更されました。 – Wikipedia: Input/output (C++)
  4. 出力演算子に<<を使うアイデアはDoug McIlroyがUNIXシェルのI/Oリダイレクト演算子(>>>)にヒントを得て提案しました。=<なども候補でしたが、=は結合方向の問題があり、<>は大小比較の意味が強すぎて混乱を招くとして退けられました。 – Qiita: std::cout << “a very short history of iostream”
  5. using namespace std;を使うとstd::countstd::listなど標準ライブラリの名前が修飾なしで使えてしまい、自作の変数名や関数名と衝突する可能性があります。cppreferenceも「std::を省略するのは習慣的に避けることを推奨する」と述べています。 – cppreference: Your first program in C++
  6. std::endlは改行文字を出力したうえでストリームバッファをフラッシュします。フラッシュはシステムコールを伴うため、ループ内など頻繁に呼ぶ場面ではパフォーマンスが大きく低下することがあります。単に改行したいだけなら'\n'で十分です。clang-tidyのperformance-avoid-endlチェックでも同様の指摘がされています。 – clang-tidy: performance-avoid-endl
  7. C++23で追加されたstd::printlnを使うと、import std; int main() { std::println("Hello, World!"); }と書けます。import std;はC++23のモジュール機能で、標準ライブラリ全体を一行でインポートできます。std::printlnはPythonのprint関数に近い感覚で使え、改行も自動で行われます。 – ModernesCpp: C++23 std::print and std::println