C++98とは、1998年に発行されたC++初の国際標準規格を指す通称です。
正式な規格名は「ISO/IEC 14882:1998」です。
C++はC言語をベースに発展してきたプログラミング言語ですが、C++98が登場する以前は、コンパイラや開発環境によって利用できる機能や細かな挙動に違いが見られることがありました。
C++98によって言語仕様と標準ライブラリが国際規格として体系的に定められたことで、C++開発における共通の基準が確立されました。
現在のC++と比べると古い規格ですが、クラスや継承、テンプレート、例外処理、STL由来のコンテナやアルゴリズムなど、現代C++につながる重要な仕組みの多くがすでに含まれています。
C++98の主な特徴
オブジェクト指向プログラミングに対応している
C++98では、クラスを利用したオブジェクト指向プログラミングが可能です。
クラスにはデータと処理をまとめることができ、カプセル化や継承、ポリモーフィズムといった設計を実現できます。
代表的な機能には、次のようなものがあります。
- クラス
- コンストラクタ
- デストラクタ
- 継承
- 多重継承
- 仮想関数
- 純粋仮想関数
- アクセス指定子
- 関数オーバーロード
- 演算子オーバーロード
例えば、仮想関数を利用すると、基底クラスのポインタを通して派生クラス側の処理を呼び出せます。
#include <iostream>
class Animal {
public:
virtual void speak() {
std::cout << "Animal" << std::endl;
}
virtual ~Animal() {}
};
class Dog : public Animal {
public:
virtual void speak() {
std::cout << "Dog" << std::endl;
}
};
int main() {
Animal* animal = new Dog();
animal->speak();
delete animal;
return 0;
}
このコードでは、animalの型自体はAnimal*ですが、実際に指しているオブジェクトはDogであるため、Dog::speak()が呼び出されます。
このような仕組みを動的ポリモーフィズムと呼びます。
テンプレートを利用できる
C++98では、関数テンプレートやクラステンプレートを利用できます。
テンプレートを使うことで、特定の型だけに依存しない汎用的なコードを記述できます。
例えば、2つの値を比較して大きい方を返す関数は、次のように記述できます。
template <class T>
T maximum(T a, T b) {
return a > b ? a : b;
}
このテンプレートは、intやdoubleなど、複数の型に対して利用できます。
#include <iostream>
template <class T>
T maximum(T a, T b) {
return a > b ? a : b;
}
int main() {
std::cout << maximum(10, 20) << std::endl;
std::cout << maximum(3.5, 2.1) << std::endl;
return 0;
}
テンプレートはC++標準ライブラリでも広く利用されており、C++における汎用プログラミングの中心的な仕組みです。
テンプレートの特殊化にも対応している
C++98では、テンプレートの明示的特殊化も利用できます。
特定の型だけ異なる処理を行いたい場合に利用されます。
template <class T>
class Sample {
};
template <>
class Sample<int> {
};
この例では、通常のSample<T>とは別に、int型専用のSample<int>を定義しています。
一方で、可変長テンプレートやエイリアステンプレートなど、後のC++で追加された機能はC++98にはありません。
C++98の標準ライブラリ
STL由来の仕組みが標準ライブラリに採用されている
C++98では、STLの設計を基礎としたコンテナ、イテレータ、アルゴリズム、関数オブジェクトなどが標準ライブラリに採用されています。
STLは「Standard Template Library」の略です。
ただし、STLとC++標準ライブラリは完全に同じ意味ではありません。
C++標準ライブラリには、STL由来の機能以外にも、iostreamやstd::string、数値関連機能などが含まれています。
vectorやlistなどのコンテナを利用できる
C++98では、複数の標準コンテナを利用できます。
代表的なものには次のようなものがあります。
std::vectorstd::liststd::dequestd::setstd::multisetstd::mapstd::multimap
例えば、std::vectorは要素数を動的に変更できる配列型のコンテナです。
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
for (std::vector<int>::iterator it = numbers.begin();
it != numbers.end();
++it) {
std::cout << *it << std::endl;
}
return 0;
}
C++98には型推論としてのautoや範囲for文がないため、イテレータの型を明示的に記述する場面が多くなります。
コンテナアダプタも利用できる
C++98では、次のようなコンテナアダプタも利用できます。
std::stackstd::queuestd::priority_queue
これらは通常のコンテナとは異なり、内部で別のコンテナを利用しながら、スタックやキューなど特定の操作方法を提供する仕組みです。
例えば、std::stackでは基本的に末尾への追加と取り出しを行います。
イテレータを利用できる
イテレータは、コンテナ内の要素を順番に参照するための仕組みです。
配列に対するポインタに近い感覚で利用できます。
std::vector<int>::iterator it;
for (it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << std::endl;
}
イテレータがあることで、コンテナの内部構造に強く依存せずに要素を操作できます。
標準アルゴリズムを利用できる
C++98には、多数の標準アルゴリズムが用意されています。
代表例は次の通りです。
std::sortstd::findstd::countstd::copystd::reversestd::transform
例えば、std::vector内の値を昇順に並べ替える場合は、次のように記述できます。
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
numbers.push_back(30);
numbers.push_back(10);
numbers.push_back(20);
std::sort(numbers.begin(), numbers.end());
for (std::vector<int>::iterator it = numbers.begin();
it != numbers.end();
++it) {
std::cout << *it << std::endl;
}
return 0;
}
C++の標準ライブラリでは、コンテナとアルゴリズムをイテレータによって結び付ける設計が重要な特徴となっています。
std::stringを利用できる
C++98では、文字列を扱うためにstd::stringを利用できます。
#include <iostream>
#include <string>
int main() {
std::string name = "C++";
std::cout << name << std::endl;
return 0;
}
C形式の文字列を直接管理する場合と比べて、文字列の代入や連結、長さの取得などを扱いやすくなります。
例えば、文字列の連結は次のように記述できます。
std::string first = "Hello";
std::string second = " World";
std::string result = first + second;
C++98で利用できる言語機能
名前空間を利用できる
C++98では、識別子の衝突を防ぐために名前空間を利用できます。
C++標準ライブラリの多くの名前は、std名前空間内に定義されています。
std::cout << "Hello" << std::endl;
独自の名前空間を作成することもできます。
namespace Sample {
int value = 100;
}
利用するときは、次のようにスコープ解決演算子::を使用します。
std::cout << Sample::value << std::endl;
名前空間は、大規模なプログラムや複数のライブラリを組み合わせる際の名前衝突を防ぐために重要です。
例外処理を利用できる
C++98では、try、throw、catchを使用した例外処理が可能です。
#include <iostream>
int divide(int a, int b) {
if (b == 0) {
throw "division by zero";
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 0) << std::endl;
} catch (const char* message) {
std::cout << message << std::endl;
}
return 0;
}
例外処理を利用すると、通常の処理とエラー処理を分離しやすくなります。
RTTIを利用できる
C++98では、RTTI(Run-Time Type Information)を利用できます。
代表的な機能は次の2つです。
dynamic_casttypeid
dynamic_castを利用すると、継承関係にある型について実行時に型を確認しながらキャストできます。
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
基底クラスのポインタや参照から派生クラスへ実行時にダウンキャストする場合などには、基底クラスがポリモーフィック型である必要があります。
一般的には、少なくとも1つの仮想関数を持つクラスがポリモーフィック型です。
ポインタへのdynamic_castに失敗すると、ヌルポインタが返されます。
一方、参照へのdynamic_castに失敗した場合は、std::bad_cast例外が送出されます。
bool型を利用できる
C++98の標準仕様では、boolが組み込み型として規定されています。
boolでは、trueとfalseの2つの真偽値を扱えます。
bool flag = true;
if (flag) {
std::cout << "true" << std::endl;
}
条件判定を明確に記述できるため、現在のC++でも基本的な型として広く利用されています。
参照型を利用できる
C++98では、参照型を利用できます。
参照は、既存の変数に対する別名として機能します。
int value = 10;
int& ref = value;
ref = 20;
この場合、refを変更するとvalueも変更されます。
関数の引数として参照を利用することもできます。
void update(int& value) {
value = 100;
}
不要なコピーを避けながら読み取り専用で値を渡したい場合には、const参照がよく利用されます。
void print(const std::string& text) {
}
constを利用できる
C++98でもconstを利用して、変更すべきでない値を明示できます。
const int MAX_SIZE = 100;
メンバ関数の末尾にconstを付けることもできます。
class Sample {
public:
int getValue() const {
return value;
}
private:
int value;
};
constメンバ関数では、原則としてオブジェクトの通常のデータメンバを変更できません。
ただし、mutableが指定されたメンバなどは例外です。
関数オーバーロードを利用できる
C++98では、引数の型や個数が異なれば、同じ名前の関数を複数定義できます。
void print(int value) {
}
void print(double value) {
}
関数を呼び出した際は、渡された引数に応じて適切な関数が選択されます。
演算子オーバーロードを利用できる
C++98では、独自クラスに対して演算子の動作を定義できます。
class Point {
public:
int x;
int y;
Point operator+(const Point& other) const {
Point result;
result.x = x + other.x;
result.y = y + other.y;
return result;
}
};
これにより、独自クラスでも+や==などの演算子を自然な形で利用できます。
C++98のメモリ管理
newとdeleteを利用できる
C++98では、動的メモリを確保するためにnewを使用します。
確保したメモリはdeleteで解放します。
int* value = new int;
*value = 100;
delete value;
配列の場合は、new[]とdelete[]を組み合わせます。
int* values = new int[10];
delete[] values;
正しく解放しないとメモリリークの原因になるため、リソース管理には注意が必要です。
RAIIが重要な設計手法となる
C++98でもRAIIは重要な設計手法です。
RAIIは「Resource Acquisition Is Initialization」の略です。
リソースの所有期間をオブジェクトの寿命に結び付け、オブジェクトの破棄時にリソースも適切に解放する考え方です。
class Resource {
private:
int* data;
public:
Resource() {
data = new int[100];
}
~Resource() {
delete[] data;
}
};
RAIIはメモリだけでなく、ファイルやロックなどの管理にも応用できます。
std::vectorやstd::stringなどの標準ライブラリも、利用者が内部のメモリを直接解放しなくてもよいようにリソース管理を行っています。
std::auto_ptrが存在する
C++98の標準ライブラリには、std::auto_ptrというスマートポインタが存在します。
ただし、現在利用されているstd::unique_ptrとは異なる特殊なコピー動作を持っています。
例えば、コピーを行うと所有権がコピー元からコピー先へ移ります。
std::auto_ptr<A> p1(new A);
std::auto_ptr<A> p2 = p1;
このような仕様は扱いにくい面があったため、std::auto_ptrはC++11で非推奨となり、C++17では削除されました。
C++98では利用できない主な機能
型推論としてのautoは利用できない
現在のC++では、次のようにautoを使って変数の型をコンパイラに推論させられます。
auto value = 10;
しかし、この意味でのautoが導入されたのはC++11です。
C++98にもautoというキーワード自体は存在しますが、当時はストレージクラス指定子として利用されるもので、現在の型推論とは意味が異なります。
C++98では、基本的に型を明示します。
int value = 10;
範囲for文は利用できない
現在のC++では、次のような範囲for文を利用できます。
for (int value : values) {
}
範囲for文はC++11で導入されたため、C++98では利用できません。
C++98では、イテレータなどを利用して要素を走査します。
for (std::vector<int>::iterator it = values.begin();
it != values.end();
++it) {
}
nullptrは利用できない
C++98にはnullptrがありません。
ヌルポインタ定数として、整数リテラルの0やNULLが利用されていました。
int* ptr = 0;
型安全なヌルポインタリテラルであるnullptrは、C++11で導入されています。
ラムダ式は利用できない
C++98にはラムダ式がありません。
現在のC++では、次のような無名関数をその場で定義できます。
[](int value) {
return value * 2;
}
ラムダ式はC++11で導入された機能です。
C++98では、通常の関数や関数オブジェクトを利用します。
右辺値参照とムーブセマンティクスは利用できない
C++98には、右辺値参照を表すT&&がありません。
そのため、ムーブコンストラクタ、ムーブ代入演算子、std::moveなども利用できません。
C++11以降ならムーブできる場面でも、C++98ではコピーが必要になる場合があります。
ただし、コンパイラによるコピー省略などの最適化が行われる場合はあります。
constexprは利用できない
C++98にはconstexprがありません。
constexprはC++11で導入された機能です。
C++98では、用途に応じてconstやenumなどを使って定数を表現していました。
可変長テンプレートは利用できない
C++11以降では、次のような可変長テンプレートを記述できます。
template <class... Args>
void function(Args... args) {
}
C++98にはこの機能がないため、テンプレート引数の個数を柔軟に変える処理はより複雑になります。
標準のスレッド機能は利用できない
C++98の標準ライブラリには、現在利用されている次のような並行処理機能はありません。
std::threadstd::mutexstd::atomic
標準ライブラリとして本格的なスレッド関連機能が追加されたのはC++11です。
C++98でマルチスレッド処理を行う場合は、OS固有のAPIや外部ライブラリなどを利用する必要がありました。
unordered_mapやunordered_setは利用できない
C++98には、次のようなハッシュベースの連想コンテナはありません。
std::unordered_mapstd::unordered_set
これらはC++11で標準ライブラリに追加されました。
C++98では、標準の連想コンテナとしてstd::mapやstd::setなどを利用します。
C++98ではコピーの設計が重要
Rule of Threeを意識する必要がある
C++98にはムーブセマンティクスがないため、リソースを所有するクラスではコピー処理が特に重要です。
代表的な考え方として「Rule of Three」があります。
リソースを管理するクラスで次のいずれかを独自に定義する必要がある場合、一般的には3つすべての定義を検討すべきだという考え方です。
- デストラクタ
- コピーコンストラクタ
- コピー代入演算子
例えば、動的メモリを持つクラスで単純なコピーを行うと、複数のオブジェクトが同じポインタを所有し、二重解放などの問題につながる可能性があります。
C++98を理解するうえでは、コピーセマンティクスと所有権管理の理解が非常に重要です。
C++98とC++03の違い
C++03はC++98の修正や明確化が中心
C++03は、C++98を全面的に作り直した規格ではありません。
C++98で発見された問題への技術的な修正や、仕様の明確化などを反映した規格です。
そのため、C++98とC++03はまとめて「C++98/03」と表現されることもあります。
C++03からC++11への変化と比較すると、C++98からC++03への変更は小規模です。
C++98とC++11の違い
C++11では多数の現代的な機能が追加された
C++11では、C++98から大幅に言語仕様と標準ライブラリが拡張されました。
代表的な違いは次の通りです。
| 機能 | C++98 | C++11 |
|---|---|---|
| クラス | ○ | ○ |
| 継承 | ○ | ○ |
| テンプレート | ○ | ○ |
| 標準コンテナ | ○ | ○ |
| 例外処理 | ○ | ○ |
| 名前空間 | ○ | ○ |
std::string | ○ | ○ |
型推論auto | × | ○ |
nullptr | × | ○ |
| ラムダ式 | × | ○ |
| 範囲for文 | × | ○ |
| 右辺値参照 | × | ○ |
| ムーブセマンティクス | × | ○ |
constexpr | × | ○ |
std::unique_ptr | × | ○ |
std::shared_ptr | × | ○ |
| 可変長テンプレート | × | ○ |
enum class | × | ○ |
static_assert | × | ○ |
std::thread | × | ○ |
std::unordered_map | × | ○ |
C++11は、現在一般に「Modern C++」と呼ばれる設計スタイルへの大きな転換点となった規格です。
C++98のメリット
古い開発環境でも利用できる場合がある
C++98は長期間利用されてきた規格であるため、C++11以降に対応していない古いコンパイラでも扱える場合があります。
特に、長期間運用されている業務システムや組み込み機器などでは、C++98やC++03相当のコードが残っていることがあります。
ただし、非常に古いコンパイラではC++98への対応自体が完全ではない場合もあります。
レガシーコードの理解に役立つ
古いC++システムでは、C++98やC++03を前提として書かれたコードが現在でも使われています。
そのため、C++98の仕様を理解しておくと、古い業務システムや組み込みシステム、ライブラリなどを保守する際に役立ちます。
C++の歴史的な設計を理解できる
C++98には、現在のC++で利用できる便利な機能が多くありません。
そのため、参照、ポインタ、コピー、デストラクタ、RAII、イテレータなど、C++の基本的な仕組みを直接扱う場面が多くなります。
C++11以降の機能がなぜ追加されたのかを理解するうえでも、C++98の知識は役立ちます。
C++98のデメリット
コードが冗長になりやすい
C++98には型推論としてのautoがありません。
例えば、現在のC++なら次のように書けます。
for (auto it = values.begin(); it != values.end(); ++it) {
}
C++98では、次のようにイテレータ型を明示する必要があります。
for (std::vector<int>::iterator it = values.begin();
it != values.end();
++it) {
}
テンプレートを多用するコードでは、型名が非常に長くなることがあります。
リソース管理が難しくなりやすい
C++98には、std::unique_ptrやstd::shared_ptrなど、現在一般的に利用されているスマートポインタがありません。
標準コンテナやRAIIを活用することは可能ですが、独自にリソースを所有するクラスではコピー処理や解放処理を慎重に設計する必要があります。
設計を誤ると、メモリリークや二重解放などの問題につながる可能性があります。
現代的なライブラリを利用しにくい
現在提供されているC++ライブラリの多くは、C++11以降を最低要件としていることがあります。
そのため、C++98に限定すると、利用できるライブラリや開発ツールが少なくなる可能性があります。
C++98は現在でも使われているのか
新規開発で採用されるケースは限定的
現在の新規開発では、利用可能なコンパイラやライブラリの条件に応じて、C++17以降など、より新しいC++規格が採用されることが一般的です。
C++98には、メモリ管理や型推論、並行処理などの面で現代C++より不便な部分が多いため、新しくC++98を選択するケースは限定的です。
レガシーシステムでは残っている
一方で、次のような分野ではC++98やC++03相当のコードを扱うことがあります。
- 古い業務システム
- 組み込みシステム
- 長期間保守されているソフトウェア
- 古いゲームやミドルウェア
- 古いコンパイラを使う必要がある環境
- 長寿命の産業機器
そのため、C++98は新規開発のためだけではなく、既存コードを理解・保守するためにも知っておく価値があります。
C++98を学ぶ際に押さえておきたいポイント
クラスとオブジェクトを理解する
C++98を学ぶ場合は、まずクラス、コンストラクタ、デストラクタ、アクセス指定子などの基本を理解することが重要です。
そのうえで、継承や仮想関数、ポリモーフィズムへ進むと理解しやすくなります。
ポインタと参照を理解する
C++98では、ポインタや参照を直接扱う場面が多くあります。
特に、オブジェクトの寿命や所有権、ヌルポインタ、const参照などの考え方は重要です。
コピーセマンティクスを理解する
C++98にはムーブセマンティクスがないため、コピーコンストラクタやコピー代入演算子を理解することが重要です。
リソースを所有するクラスでは、Rule of Threeも押さえておく必要があります。
STLとイテレータを理解する
std::vectorやstd::mapなどの標準コンテナと、イテレータ、標準アルゴリズムの関係を理解すると、C++98の標準ライブラリを効率的に扱えるようになります。
C++98についてのまとめ
C++98は、1998年に発行されたC++初の国際標準規格です。
正式な規格名は「ISO/IEC 14882:1998」です。
クラス、継承、仮想関数、テンプレート、例外処理、RTTI、名前空間、std::string、STL由来のコンテナやアルゴリズムなど、現在のC++につながる重要な機能が数多く規定されています。
一方で、型推論としてのauto、nullptr、ラムダ式、範囲for文、ムーブセマンティクス、constexpr、std::unique_ptr、標準スレッド機能など、現代C++では一般的な機能は利用できません。
そのため、現在のC++と比較するとコードが冗長になりやすく、リソース管理やコピー処理にも注意が必要です。
新規開発でC++98を選択するケースは少なくなっていますが、レガシーシステムの保守や古いC++コードの理解、さらに現代C++の機能が導入された背景を理解するうえでは、今でも知っておく価値のある規格です。
以上、C++98の特徴や仕様についてでした。
最後までお読みいただき、ありがとうございました。
