はじめに
本稿はAdvent calendar 2023のC++カテゴリ1日目である。
最初の日ということも考慮し、あまり深々と掘らずに基本的なことから考えていく。
C++の言語仕様とかC++23以降のお話とかはもっと専門的な方がやってくれるはずなのでそれに期待(期待
C++で開発する
C++でいざ開発するとなった場合、おおよそWindowsであればVisual StudioというIDEを適当なオプションを入れて走らせるのがもっとも手間をかけずに開発環境を整えられる手段である。だが今のWindowsであればmingwという選択肢もあるし、それこそWSLgでLinuxネイティブなGUIをWindowsで動かすこともできる。Visual studioのインストールオプションをいじればmingwはともかくWSLg越しのLinux開発やMAC OSのアプリ開発、ひいてはAndroidやiOSアプリの開発も可能である。
だがC++開発の環境の中で、その行く手を大きく阻むのが「ビルドシステムの構築」と「サードパーティーのライブラリを追加する労力」である。前者は古来から作り上げられてきたが、後者はつい最近まで手作業か、自力で依存系解決用のスクリプトを組むものであった。
ビルドシステム
IDEとは言うものの、そのコアにはビルドシステムが存在する。Visual Studioが作るプロジェクトは、実はmsbuildと呼ばれる、Microsoftがオープンソースで提供している、ビルドシステムを動作させるXMLファイルに紐づいたファイル群である。
試しにVisual Studioが吐き出すvcxprojという拡張子のファイルを、MSVCのビルドツール設定が行われたコマンドライン上でmsbuild filename.vcxproj
と打つとプロジェクトのビルドが走り始める。XMLファイル上には、そのプロジェクトの設定としてVisual Studio上のGUIで行った情報がふんだんに盛り込まれており、msbuildはこの情報をもとにビルドを行う。
LinuxなりMAC OSなりでもこういった仕組みはmakeファイルやあるいはninjaなどの形式で組まれる。これらが動作するためのスクリプトは手動で作ることもできるが大抵はautomakeやCMake、mesonといったビルドシステム構築ツールによって形成するもの。Visual StudioのIDEも実はmsbuild形式に特化したビルドシステム構築ツールを内蔵しているのである。
C++はこの仕組みを使用してコンパイルしたバイナリを実行可能な形式に集約し、しかるべき場所に配置するのが一般的である。
巷に多くあるスクリプト言語であればより汎用的にコンパイルルールを作成できそうなものであるが、それでもなおこれらのビルドシステムが使われてきたのは、C++のプログラムをビルドするために必要な情報収集や設定をビルドシステム構築ツールがすべて担って自動的に行ってくれるためである。
なおVisual Studioはmsbuildの構築ツールと言ったが、現状、多岐にわたる開発機能を備えたテキストエディタといったほうが早い。筆者はこれを使って小説執筆を行ったりもした。
冗談はともかく、このツールではmsbuildに囚われずCMakeと連携できるし、Pythonとの連携でmesonを動作させることもできる。さすがにmesonはやり過ぎなので、CMakeとの連携ができる点は抑えておきたい。
ライブラリ管理システム
その昔は、いくらIDEやらRADやらといっても、その元々持っているライブラリではない外部のものを導入するためには、プロジェクトの設定に対象パスを手動で組み込んだり、プロジェクトのサブフォルダに対象のライブラリを含むなどする必要があった。確かにLinuxなどはシステムそのものが開発ライブラリを兼ねていて、「lib〇〇-dev」なり「lib〇〇-devel」なりのパッケージを入れれば特に考えることなくそれらの外部ライブラリを使用できる。だがシステムフォルダにソースコードを持てない、APIが中心のWindowsにおいてのC++開発は、相応の労力を必要とするものであった。またたとえLinuxでも、ディストリビューションのリポジトリにあるバージョンでは古い場合、これを解消するにはライブラリのベンダーにあるソースをビルド・インストールする必要があった。msbuild形式のプロジェクトであればnugetが限定的に使用できるが、ほぼライブラリはWindows開発専用の様相である。
そのため、C++におけるRustのCargoのようなものが求められた。それがConanであったりvcpkgである。特にvcpkgはMicrosoft謹製のライブラリ管理ツールであり、特に外部ライブラリ導入の労力の大きなWindowsにおけるC++開発を大きく助けてくれるものとなっている。
vcpkg
vcpkgはMicrosoftが作成した、C++用のライブラリ管理システムである。しかるべきvcpkgインストールののち、プロジェクトの中にvcpkg.jsonを設けて依存するライブラリ名を入れるだけで、必要な依存性の解決を行うライブラリのインストールが自動で行われる。VisualStudio越しのmsbuild形式でも、CMakeからでも使用できる。
これを使えばboostも、Gtkも、Qtも、特に公式サイトから必要なファイルをダウンロード・インストール等することなく使用できる。vcpkgへの設定を通しさえすれば、その管理化にインストールされたファイルは特別なことをせずに使用できる。
実演:vcpkgの力を借りたVisual Studio上でCMakeを走らせてのGtkスケルトン作成
vcpkgの優れているところのひとつとして、クロスプラットフォームにまつわる各種困難な部分を集合知の成果で解決してくれる点である。それこそ、クロスプラットフォームであるにも関わらず自力でMSVCで動作させるには多くの障害に阻まれるGtkのライブラリ構築を、一手に引き受けて行ってくれる。筆者は過去に手動でMSVCで動作させるためのGtkの依存ライブラリを組もうと試みて失敗しているため、これが達成されたときの感動はひとしおであった。
この実演では、少し手を加えればMacやLinuxでも動作するであろうGtkのスケルトンを、Windowsネイティブ環境で作成する。なおC++企画であるため、生のGtkではなくC++ラッパーであるGtkmmを使用する。
Visual Studioを入れる。
公式から適当なバージョンのCommunity版をダウンロードしてインストールする。
今回の解説は現行のVisual Studio 2022を前提とするため、例えば2019以前ではvcpkg関連を自力でインストールする必要があるなどするため手順が異なってくる。
オプション設定で、「C++によるデスクトップ開発」を選択し、個別のオプションでCMakeを選択する。
またvcpkgも選択して導入しておく。
プロジェクトの作成
CMakeのプロジェクトを適当に作る。なおある不具合の関係から、ソースフォルダはCドライブ直下に入れておく。
・ある不具合
vcpkgでgtkmmをインストールしている途中で、特別にビルドエラーらしいメッセージがないのに停止した場面があった。単純にソースのフォルダを置いてある場所が深かったため、コマンドラインに当てがうパスの文字数が長くなりすぎて、コマンドの字数制限を超えてしまったためだった。
そのためプロジェクトはCドライブ直下に作成し、プロジェクト名もコンパクトにすることをお勧めする。
作成したプロジェクトの完成版フォルダ構成はこんな感じになる。
作成した地点ではvcpkg_installedとvcpkg.jsonはない。
vcpkg_installedは後述するvcpkg install
を走らせた後に作られる。
vcpkg.jsonは上記のために必要な設定ファイルとして追加する必要があるのでこれも後述。
CMakePresets.jsonの設定
共通項のcacheVariables
にCMAKE_TOOLCHAIN_FILE設定をする。
CMAKE_TOOLECHAIN_FILEの場所は、Visual Studioからコンソールを走らせたあと、コマンドプロンプトであればecho VCPKG_ROOT
で、PowerShellであれば$env:VCPKG_ROOT
で表示できるパスに、scripts/buildsystems/vcpkg.cmake
を追加したものである。
{
"configurePresets": [
{
"name": "windows-base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}",
"cacheVariables": {
"CMAKE_C_COMPILER": "cl.exe",
"CMAKE_CXX_COMPILER": "cl.exe",
"CMAKE_TOOLCHAIN_FILE": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/vcpkg/scripts/buildsystems/vcpkg.cmake",
"CMAKE_WIN32_EXECUTABLE": true
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
vcpkgの設定
vcpkg.jsonをプロジェクトルートに追加し、以下のように設定する。
{
"name": "CMake-gtkmm",
"version": "0.0.1",
"builtin-baseline": "【ハッシュ値】",
"dependencies": [
"gtkmm",
"pkgconf"
]
builtin-baselineのハッシュ値はこの地点ではわからないので、次項で解説する。
あらかじめのインストール
全体のビルド工程に含まれるが、いったん依存ライブラリをインストールする。
Visual Studio上からコンソールを立ち上げて、vcpkg install
を実行するとbuiltin-baseline
用のハッシュが流れるので、これをvcpkg.jsonの該当部分に入れてもう一度vcpkg install
を走らせる。すると設定したライブラリの依存関係を解決するためのライブラリリストが一通りビルドされインストールされる。
pkg-configの有効化
GtkmmをCMakeで使用するためには直接find_packageできない関係から、PkgConfigプラグインを走らせる必要がある。
ビルド工程中に流れるmsys root
で表示されるパスをたどってpkg-configの場所を探り当て、これをソースルートのCMakeListsのPKG_CONFIG_EXECUTE変数に割り当てると、find_package(PkgConfig)を走らせることができるようになる。
下記プロジェクトルートのCMakeListsのパスはそれで指定しているが、msys2の下のフォルダ名にあるハッシュのような部分(下記であれば6f3fa1a12ef85a6f
のところ)は各環境で異なるので各自のフォルダ名に合わせること。
CMakeListsの設定
# CMakeList.txt : 最上位の CMake プロジェクト ファイル。グローバル構成を行います
# また、サブプロジェクトをここに含めます。
#
cmake_minimum_required (VERSION 3.8)
# サポートされている場合は、MSVC コンパイラのホット リロードを有効にします。
if (POLICY CMP0141)
cmake_policy(SET CMP0141 NEW)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>")
endif()
project ("CMake-gtkmm")
set(PKG_CONFIG_EXECUTABLE "C:/Users/exli3/vcpkg/downloads/tools/msys2/6f3fa1a12ef85a6f/mingw32/bin/pkg-config.exe")
find_package(PkgConfig)
pkg_check_modules(GTKMM gtkmm-4.0)
if(WIN32 AND MSVC)
add_compile_options(-utf-8)
endif()
# サブプロジェクトを含めます。
add_subdirectory ("CMake-gtkmm")
# CMakeList.txt : CMake-gtkmm の CMake プロジェクト。ソースを含めて、次を定義します:
# プロジェクト専用ロジックはこちらです。
#
link_directories(
${GTKMM_LIBRARY_DIRS}
)
include_directories(
${GTKMM_INCLUDE_DIRS}
)
# ソースをこのプロジェクトの実行可能ファイルに追加します。
add_executable (CMake-gtkmm "CMake-gtkmm.cpp" "CMake-gtkmm.h")
target_link_libraries(CMake-gtkmm
${GTKMM_LIBRARIES}
)
if (CMAKE_VERSION VERSION_GREATER 3.12)
set_property(TARGET CMake-gtkmm PROPERTY CXX_STANDARD 20)
endif()
# TODO: テストを追加し、必要な場合は、ターゲットをインストールします。
スケルトンのソースを構築。
スケルトンを動作させるためのgtkmmのソースを作る。
// CMake-gtkmm.h : 標準のシステム インクルード ファイル用のインクルード ファイル、
// または、プロジェクト専用のインクルード ファイル。
#pragma once
#include <iostream>
// TODO: プログラムに必要な追加ヘッダーをここで参照します。
#include <gtkmm/window.h>
#include <gtkmm/application.h>
// CMakeProject-gtkmm.cpp : アプリケーションのエントリ ポイントを定義します。
//
#include "CMake-gtkmm.h"
namespace CMakeGtkmm {
class MyWindow : public Gtk::Window {
public:
MyWindow();
};
MyWindow::MyWindow() {
set_title("Hello Work!");
set_default_size(600, 400);
}
}
int main(int argc, char** argv) {
auto app = Gtk::Application::create("org.gtkmm.example.base");
return app->make_window_and_run<CMakeGtkmm::MyWindow>(argc, argv);
}
プロジェクトを一通り完成させたのちにビルド完了すれば、以下のようなスケルトンを表示可能なはずである。
コンソール窓を開かせない方法
ただmain関数のままWindows開発で走らせると、どうしてもコンソール窓が出てしまう。
windows以外の
- CMakePresetsのcacheVariablesにてCMAKE_WIN32_EXECUTABLEをtrueに設定する。 (本記事ではすでに設定済み)
-
main
関数をWinMain
関数にする。 -
make_window_and_run
メンバ関数の引数に渡すための値は、WinMain
関数の引数であるlpCmdLine
を解釈する。
// CMake-gtkmm.h : 標準のシステム インクルード ファイル用のインクルード ファイル、
// または、プロジェクト専用のインクルード ファイル。
#pragma once
#include <iostream>
// TODO: プログラムに必要な追加ヘッダーをここで参照します。
#include <gtkmm/window.h>
#include <gtkmm/application.h>
#include <Windows.h>
// CMakeProject-gtkmm.cpp : アプリケーションのエントリ ポイントを定義します。
//
#include "CMake-gtkmm.h"
namespace CMakeGtkmm {
class MyWindow : public Gtk::Window {
public:
MyWindow();
};
MyWindow::MyWindow() {
set_title("Hello Work!");
set_default_size(600, 400);
}
}
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ PSTR lpCmdLine, _In_ int nCmdShow) {
int argc;
LPWSTR* argv;
argv = CommandLineToArgvW(GetCommandLineW(), &argc);
auto app = Gtk::Application::create("org.gtkmm.example.base");
return app->make_window_and_run<CMakeGtkmm::MyWindow>(argc, (char**)argv);
}
C++が使われる場面におけるもの
C++は大量の仕様に裏付けられた高機能さと、低レベル領域にも触れられる権限の強さを持った、パフォーマンス自慢の言語である。AtCoder等競技プログラミングで高速度実行を達成している上位に大量のC++erが名を連ねるのだからその実力も確かなものである。
ただハードウェア要件の厳しいデバイス構築の場面ではCに、速度そこそこ以上で開発性を上げるという点でC#やJavaなどのマネージ言語に、メモリ安全性の面でGoやRustなどの新興バイナリ言語に、速度度外視で開発速度を高めるという点で多くのスクリプト言語群に活躍の場を取られている。
それでもなおC++が生きるのは、よりパフォーマンスの必要なアプリケーションを作る場合に、Cの資産を大きな労力なく利用できることによってハードウェア支援のアドバンテージを大きく受けられる時である。
おそらくは業務用のプロジェクトとして新たに組むとした場合の需要は減ったのだろうが、巷の根幹をなす部分の至る所でC++は使用されている。Webブラウザにも、Webサーバーにも、データベースサーバーにも。ハードウェア要件の厳しいゲームはほぼC++の独壇場であると言える。
C++の最先端を追えるのは、それこそC++学者と呼べるレベルの専門家であろうが、ほんのささやかに、ちょっとしたアプリを作るところから始めるにはそれほどの習熟は必要ない。おそらくその行く手を真に阻むのは、言語仕様の難解さや開発環境作り以上に、重量級の機能を網羅するために各ライブラリがそろえた莫大な量のclass及びstructと、その中で定義しなければならないメンバ変数の把握と、各メンバ関数の挙動、また言語仕様によって彩られた各種テンプレートを頭の片隅に詰め込む行為の面倒さであろう……