はじめに
C言語から派生したオブジェクト指向プログラミング言語であるC++は、21世紀に入ってまったく別物とも言えるプログラミング言語に成長していきました。それは、Modern C++と称されています。1990年代にC++を触っていたプログラマが現在の仕様を知れば、隔世感に苛まれるのではないでしょうか。本連載では、かつてはC++をたしなんでいたという方、今からC++言語を始めるという方に向けて、Modern C++らしい言語仕様をピックアップし紹介していくことで、今のC++言語の姿を理解していただきます。
対象読者
- かつてはC++をたしなんでいたという方
- 今からC++言語を始めるという方
- モダンなプログラミング言語のパラダイムに興味のある方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
-
macOS Sonoma/Windows 11
- Xcode Command Line Tools 2395
- MinGW GCC 9.2.0
constexprとnullptr
20世紀のC++プログラマ(筆者)が、今(21世紀)のC++を知ってあっと驚く連載の第6回です。今回は、最近のプログラミング言語では当たり前の、モダンな言語仕様をピックアップして紹介します。まずは、シンプルな話としてconstexprとnullptrあたりから始めましょう。
[NOTE]C++プログラムのコンパイル
以降のサンプルは、基本的に「gcc -o 出力先ファイル名 ソースファイル名」でコンパイルできますが、環境によってはライブラリやC++バージョンの指定が必要になることがあります。コンパイルやリンクでエラーが発生するときには、以下のようにライブラリの指定(-lstdc++)、C++バージョンの指定(-std=c++11)の追加を検討してください。
% gcc -o sample sample.cpp -lstdc++ -std=c++11 // c++14, c++17, c++20など
定数定義はconstexprで[C++11]
C/C++言語にはconst修飾子というものがありまして、変数宣言に適用した場合にはその変数を不変(immutable)とすることができます。例えば、以下のような感じです(マクロを定数代わりに使うということもよく行われていましたが、全く安全でないのでここでは割愛します)。
const int a = 100; a = 200; // コンパイルエラー // cannot assign to variable 'a' with const-qualified type 'const int' const int b = rand(); b = 300; // コンパイルエラー // cannot assign to variable 'b' with const-qualified type 'const int'
const修飾されたa(あるいはb)には再代入できない、というエラーになります。aとbで異なるのは、aがリテラルによる初期化、bが関数による初期化ということです。const修飾子では、両者を特に区別せず、とにかく再代入不可とします。
C++ 11では、前者のようなケースにはconstexpr指定子を使って明確に区別することになりました。
constexpr int c = 200; c = 400; // コンパイルエラー constexpr int d = rand(); // コンパイルエラー // constexpr variable 'd' must be initialized by a constant expression
前者は、const修飾子の例と同じで再代入時にエラーとなりますが、後者は宣言時に定数での初期化が必要というエラーとなります。つまり、const修飾子とconstexpr指定子には以下の違いがあるということです。
- const:初期化子が定数か変数かは問わない。とにかく変更できない変数とする
- constexpr:初期化子は定数でなければならない
constexpr指定子によって不変とされた変数は、コンパイル時に値が決まるので、コンパイル後のバイナリに直値として埋め込まれます(つまり実行効率の向上になります)。const修飾子によって不変とされた変数は、初期化子や最適化によってケースバイケース(メモリに置かれたり、直値となったり)となります。よって、明らかな定数にはconstexpr指定子を使うのが、C++ 11以降では正しい使い分けと言えます。
なお、constexpr関数というものもあり、これは入力も戻り値もconstexprとなる関数です。この関数による初期化はOKです。ただしあくまでもコンパイラが追跡できて、最終的な結果が定数であると判断できた場合のみです。以下のリストでは、正しいconstexpr関数と、呼び出せないconstexpr関数の例を示しています。
// 入力がconstexprで計算結果もconstexprとできるので呼び出し可能 constexpr bool is_greater(int a, int b) { a += 3; return (a > b)? true : false; } // 入力がconstexprでも関数内でconstexprでない関数呼び出しがあるので呼び出し不可 constexpr bool is_lower(int a, int b) { a += rand(); // コンパイルエラー return (a < b)? true : false; } constexpr bool greater = is_greater(5, 4); // OK constexpr bool lower = is_lower(8, 2); // 関数は呼び出し不可
NULLポインタはnullptrで[C++11]
C/C++言語では、長い間NULLポインタの明確な定義がありませんでした。特にC言語では、プログラマによって以下のように勝手に定義して使っていたのではないかと思います。
#define NULL ((void *)0) // あるいは単に「0」とするとか… void *ptr = NULL;
NULLポインタを便宜上アドレス0番地へのポインタとするわけです。このため、NULLポインタでメモリに書き込んでしまったことをプログラム終了時に検出し、Null pointer assignmentといったエラーを吐き出す処理系もありました。C++ 03では少し改善されて、NULLマクロが定義済みとなりました。なので、上のリストのように#defineしてコンパイルすると再定義エラーになります。ちなみに、このNULLマクロは、gccでは__nullという値で定義されています。__nullには移植性はないようなので、そのまま使うのは避けた方がいいでしょう。
ただ、いつまでもこのようなマクロを使うのはどうかということで、C++ 11において言語仕様としてnullptrが定められました。nullptrは当たり前ですが整数型などに変換されないので、安全に使うことができます。かつて、\0をヌル文字と表現しているせいもあって、ポインタのNULLとヌル文字のNULLがごっちゃになっているケースがありました。NULLが0に定義されていると、どちらにでも適用できてしまうので、いらぬバグの元でした。
nullptrを使うと、ポインタ変数を明示的にNULLポインタとして初期化できます。また、連載第3回で紹介したスマートポインタも、コンストラクタによって暗黙的にnullptrに初期化されます。
int *nptr = nullptr; unique_ptr<int> int_ptr; shared_ptr<int> str_ptr; *nptr = 1; // 実行時segmentation fault
なお、このリストの最終行はnullptrによる書き込みとなりますが、コンパイル時には指摘してくれずに実行時にエラーとなります。このように、コンパイル時にnullptrを追跡してくれるわけではなさそうです。