C++のstd::string型クラス静的メンバー変数に文字列定数を定義するいくつかの方法
はじめに
10年とちょっと前、C++(C++98/03規格)でプログラミングする際に文字列定数をどうやって表現しようかと考え、クラスの静的メンバ変数にconst修飾子を付けました。
// Hello.h class Hello { public: void greet(); private: static const std::string MESSAGE; };
- ヘッダーファイルのクラス定義に静的メンバ定数(constなメンバ変数)を含める場合、組込み型は値を記述した定義を記述できますが、非組込み型は値を記述できず変数名までの宣言を記述し、実装ファイルに初期化を定義します。
// Hello.cpp const std::string Hello::MESSAGE = "Hello, World!"; void Hello::greet() { std::cout << MESSAGE << std::endl; }
LinuxのGCCで、__attribute__((constructor))
を付けた関数内でこのクラスの静的メンバ変数を使用するコードを書いていましたが、Red Hat Enterprise Linux 7(CentOS 7)にビルドを変更(GCCのバージョンがGCC4.4→4.8)したところ、プロセス起動時にエラー(Segmentation fault)となってしまいました。この直接の原因追究が難航したので、回避策の検討としてクラスの静的メンバに文字列定数を定義する別な方法を調べました。
調査の範囲
- Linux(CentOS 7)でGCCを使用します。GCCはOS標準搭載の4.8系と、Software Collections(SLC)提供の8系および9系で確認します。
std::string
型の定数を用意し、値だけでなく参照も同一とすることを前提とします。- モダンC++(C++11以降)の規格を調査対象に含めます。
調査結果
C++17で導入された次の2つの機能を用いる方法がある。
- constexprでstd::string_viewを用いる
- inline指定をする
constexprとstd::string_view(C++17)
constexpr
を使ってクラスの静的メンバ変数に文字列定数を定義する方法は、C++11ではエラーとなりました(後述)。
これは、C++17で導入されたstd::string_view
により解決します。
// Hello.h class Hello { public: void greet(); private: static constexpr std::string_view MESSAGE { "Hello, World!" }; };
inline(C++17)
inlineを使ってクラスの静的メンバ変数に文字列定数を定義します。
// Hello.h class Hello { public: void greet(); private: inline static constexpr std::string MESSAGE { "Hello, World!" }; };
うまくいかなかったアプローチ
namespace静的定数(C++98)
ヘッダーファイルにnamespace静的定数を定義する方法は、ヘッダーファイルをインクルードした翻訳単位毎にオブジェクトが割当てられるため、値は一緒だがアドレスが異なってしまいます。
// Hello.h namespace hello { static const std::string MESSAGE = "Hello, World!"; }
- Hello.h をincludeするAlfa.cppとBravo.cppがあると、Alfa.cpp内の関数で
hello::MESSAGE
のアドレスを取り出し、Bravo.cpp内の関数でhello::MESSAGE
のアドレスを取り出して両者を照合すると異なるアドレスとなります。
constexpr(C++11)
C++11では、constexprキーワードが導入されコンパイル時定数が定義できるようになりました。 しかし、std::string (実際にはbasic_string)がリテラル型ではないとコンパイルエラーになります。
error: the type 'const string {aka const std::basic_string<char>}' of constexpr variable 'Hello::MESSAGE' is not literal static constexpr std::string MESSAGE = "Hello, World!";
環境メモ
GCCのC++11/14/17対応バージョン
CentOS 7でGCCを使い分ける方法
CentOS 7はOS標準搭載GCC(CentOSのパッケージリポジトリで提供されるGCC)のバージョンは4.8です。 それより新しいGCCバージョンを使う場合、Software Collectionsリポジトリから取得するのが楽です。
Software Collectionsは、Red Hat Enterprise Linux、Fedora、CentOS、およびScientific Linux向けにソフトウェア集を提供するコミュニティ・プロジェクトです。 CentOSのyumリポジトリ(extras)に、Software Collectionsを利用するyum設定を含むパッケージ centos-release-scl および centos-release-scl-rhが含まれます。 GCC 8や9を利用するには、centos-release-scl-rhをインストールしてから、devtoolset-9-gcc-c++ 等をインストールします。
~$ sudo yum install centos-release-scl-rh : ~$ sudo yum install devtoolset-9-gcc-c++ :
Software Collectionsからインストールしたソフトウェアセットは次のコマンドで確認できます。
~$ scl -l devtoolset-8 devtoolset-9 rh-git218
Software CollectionsからインストールしたGCCは、/opt/rh/devtoolset-9/root/ 以下に展開されます。ここには環境変数等は通っていないので、利用するにはひと手間必要です。
一時的に環境変数等が設定されたbash対話環境を使用する
~$ scl enable devtoolset-9 bash ~$
~$ source scl_source enable devtoolset-9 ~$
永続的に環境変数等を設定する
そのマシンのユーザー全ての環境を変更します。
~$ sudo ln -s /opt/rh/devtoolset-9/enable /etc/profile.d/devtoolset-9.sh
ビルドツール Gradle
コマンドラインで直接コンパイルコマンド、リンクコマンドを叩くのは大変、でも Makefile を書くのも手間、というときにJavaがちょっとわかるプログラマー向けの推奨ビルドツールがGradleです。Gradleは、Javaのプログラムをビルドする3大ツールの一つとして有名ですが、C++のビルドにも対応しています。今回のように小さな検証プログラムをささっとビルドして実行するにはとっても楽です(共有ライブラリの作成も簡単)。
Gradleのインストールと設定
GradleはJavaで書かれたツールなので、実行にはJava(JDK 8以降)が必要です。 OS標準リポジトリからJDK 8をインストールします(パッケージ名はJDK 8の内部バージョンである1.8.0の表記となっています)。
~$ sudo yum install java-1.8.0-openjdk :
Gradleをインストールします。Gradleは Yumパッケージにはないので、手作業でダウンロードしインストールします。
https://gradle.org/releases/ から最新のバイナリをダウンロードします。 インストール手順に沿って、/opt/gradle の下に zipファイルを展開します。
~$ sudo unzip gradle-6.5.1-bin.zip -d /opt/gradle
環境変数PATHを、/opt/gradle/gradle-6.5.1/bin に通します。ローカルマシンの全ユーザーに永続的に設定するなら、/etc/profile.d/gradle.shを作成します。
export PATH=$PATH:/opt/gradle/gradle-6.5.1/bin
簡単Gradleビルドプロジェクト作成
ビルドプロジェクトのディレクトリを作成し、ビルド定義ファイル build.gradle を作成します。
~$ mkdir juliet ~$ cd juliet juliet $ emacs build.gradle :
- build.gradle
plugins { id 'cpp-library' id 'cpp-unit-test' }
これは、共有ライブラリファイルを作成するプロジェクトで、テストコードの作成と実行も定義するビルド定義ファイルとなります。 ビルドするソースファイル名やインクルードパスなどは何も書いていませんが、これはデフォルトのディレクトリに放り込んでおけば勝手にビルドしてくれます。 デフォルトのディレクトリは次です。このディレクトリ構成を作成し、ソースファイル、ヘッダーファイル、テストソースファイルを配置します。
juliet/ +-- build.gradle +-- src/ +-- main/ | +-- cpp/ | | +-- ソースファイル(Hello.cpp) | +-- public/ | +-- ヘッダーファイル(Hello.h) +-- test/ +-- cpp/ +-- テストコードのソースファイル(HelloTest.cpp)
src/main の下にあるファイルを共有ライブラリファイルにビルドし、test/mainの下にあるファイルを実行ファイルにビルドし共有ライブラリファイルにリンクして実行します。 C/C++プログラマーには奇異に映る構成かもしれませんが、ビルド定義で好みのディレクトリに設定することは可能です。
テスト実行(依存するコンパイル・リンクも自動的に行われる)は次のコマンドです。
juliet$ gradle runTest > Task :runTest Hello, world! BUILD SUCCESSFUL in 2s :
ビルド結果は build ディレクトリ下に生成されます。
juliet/ +-- build/ +-- exe/ | +-- test/ | +-- julietTest
プロジェクトの基点ディレクトリ名にちなんだビルド結果ファイルが生成されます。 このファイルには、テストコードとソースファイルとが1つにリンクされています。
ライブラリのビルドは次のコマンドです。
julite$ gradle build :
ビルド結果は build ディレクトリ下に生成されます。
juliet/ +-- build/ +-- lib/ | +-- main/ | +-- debug/ | +-- libjuliet.so
Gradleメモ
gcc -v でgccのバージョンを出力するメッセージが日本語化されているとgradleがgccを見つけられません。 ロケールを英語に切り替えます。
juliet$ LC=ALL=C bash juliet$
- コンパイルオプションを指定したい
build.gradle に以下を追記します。
tasks.withType(CppCompile) { compilerArgs = [ '-std=c++11' ] }
おわりに
C++であれこれ調べたのは5年振りくらいです。C++のコードをあれこれ書いたのは10年よりちょっと前でまだC++11規格が正式採択前です。 この10年でC++も随分変わっていることが実感できました。