Daily bit(e) of C++ | Advent of Code 2023: Day Zero
Daily bit(e) of C++ #331, Bleeding edge C++ development environment for Advent of Code 2023.
If you have been diligently following this series, you might already know that (by popular demand) the series will switch to Advent of Code 2023 starting December 1st.
Advent of Code is an enjoyable activity that is great for testing out a new language or, in the case of C++, bleeding-edge language features.
However, that brings up a problem. Your development environment might be using a stable version of a compiler. However, fret not; I have prepared a very simple setup that will give you the bleeding edge versions of GCC and Clang, which (when combined with CMake) both support C++20 modules. This is conveniently packaged in a template GitHub repository, so all you need is VSCode and Docker on an x86-64 or arm64 system with any OS.
Quickstart
Create a repository from the template.
Navigate to: https://github.com/HappyCerberus/aoc-2023-cpp
Click the Use this template button (if you don’t see it, you might need to zoom out) and follow the steps.
Open the repository in VSCode.
Open VSCode.
Select View > Command Palette
Write in Git: Clone, select and confirm.
Either paste in the URL of your repository or select Clone from GitHub, which will list your GitHub repositories.
After the repository is cloned, selecting Open will open the repository in the current VSCode window.
Confirm that you trust me.
In a pop-up, confirm that you want to re-open the repository in a Container.
The CMake extension will ask you to pick a kit, select Clang 17.0 (not Clang-cl)
CMake will also bother you with a recent change; you can select “Don’t ask again”.
Write code
At this point, you have a fully working development environment.
The main CMake file is configured to use the GCC 14 version of the standard library, and the selected kit makes it build with Clang 17.
The easiest way to add more code is by creating a new directory inside aoc2023, and re-using the CMakeLists.txt from aoc2023/demo.
You can build/run/debug by clicking the CMake icon in the left panel. Note that for code completion to work for a newly added file, it needs to be compiled at least once.
The configuration
The configuration is based on devcontainer support in VSCode.
The Docker image we are using, in this case, is a custom-built one, with GCC 14 built from sources, GDB 13.2 built from sources, CMake built from sources, Go 1.21.0 from the official installer, Bazelisk from the official installer, the full LLVM 17 suite from the LLVM-provided Debian packages (these are built from trunk) and finally Ninja from the official Debian packages.
Unfortunately, the image isn’t built on GitHub actions because even the GCC build times out for arm64, mainly because arm64 is emulated using QEMU. Therefore, you either have to trust me or build it yourself. Here is the repository with the Docker configuration and some basic scripts to replicate my setup. If you want to use the Docker image for other purposes, here is the corresponding DockerHub repository.
C++20 modules
The main goal that I set myself for this configuration was to get C++20 modules working. Now, I have to preface this with a big caveat: the support in compilers is still very experimental, and you will probably run into limitations.
However, we can get C++20 modules through CMake, using Clang as the compiler and libstdc++ as the standard library. We could also use GCC as the compiler; however, this would mean we couldn’t use clangd as the code-completion backend for VSCode. Our only option would be the built-in Intellisense in the C++ extension, which still doesn’t properly support C++20 ranges, not to mention modules.
At least by combining Clang with the libstdc++, we get solid C++20 and reasonable C++23 (and some C++26) support in both the compiler and standard library.
However, we have to jump through a few hoops to get this working correctly.
VSCode configuration
The first part of the VSCode configuration describes the devcontainer in .devcontainer/devcontainer.json. This covers which Docker image should be used and which extensions should be installed inside the image once it is running.
{
"image": "happycerberus/devenv-trunk:latest",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"ms-vscode.cpptools-themes",
"ms-vscode-remote.remote-containers",
"ms-vscode.cpptools-extension-pack",
"ms-vscode.cmake-tools",
"BazelBuild.vscode-bazel",
"llvm-vs-code-extensions.vscode-clangd"
]
}
}
}
The second part covers the configuration for the extensions and VSCode itself.
First, because we use clangd for code completion, we must disable the IntelliSense in the base C++ extension. The second part covers the clangd itself, pointing it to the correct binary and location of the compile_commands.json file, which CMake will generate. Lastly, we configure CMake to the correct directory and pass through information about the architecture (we will need this in our CMakeLists.txt).
{
"C_Cpp.intelliSenseEngine": "disabled",
"C_Cpp.clang_format_path": "/usr/bin/clang-format-17",
"clangd.path": "/usr/bin/clangd-17",
"clangd.arguments": [
"--background-index",
"--compile-commands-dir=${workspaceFolder}/build/"
],
"cmake.sourceDirectory": "${workspaceFolder}/aoc2023",
"cmake.configureOnOpen": true,
"cmake.options.statusBarVisibility": "visible",
"cmake.configureSettings": {
"TARGET_ARCH": "${buildKitTargetArch}"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-vscode.cpptools",
}
CMake configuration
The final part of the puzzle is the CMake configuration. This is where we ensure we use the GCC libstdc++, specifically, the version installed with GCC 14. The main hitch is that we need to set up the correct include paths which differ across architectures (this is where we use the above passthrough of the architecture).
cmake_minimum_required(VERSION 3.28)
project(demo CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_CXX_EXTENSIONS ON)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_GENERATOR "Ninja")
add_compile_options(-pedantic -Wall -Wextra -stdlib=libstdc++)
# Use GCC standard library from GCC 14.0.0
include_directories(BEFORE SYSTEM "/usr/local/include/c++/14.0.0")
if(${TARGET_ARCH} STREQUAL "x64")
include_directories(BEFORE SYSTEM
"/usr/local/include/c++/14.0.0/x86_64-linux-gnu/")
else()
include_directories(BEFORE SYSTEM
"/usr/local/include/c++/14.0.0/aarch64-linux-gnu/")
endif()
add_subdirectory(demo)
Note that setting up system configuration inside your CMake is not good practice; however, in this case, the objective is to ensure that CMake works correctly in this self-enclosed environment.
The included version of CMake supports C++20 modules, and with the above configuration, we are ready to use modules.
The following is the configuration for the example in the template repository; note the CXX_MODULES directive:
add_library(input)
target_sources(input PUBLIC FILE_SET CXX_MODULES FILES input.cc)
add_library(part_one)
target_sources(part_one PUBLIC FILE_SET CXX_MODULES FILES part_one.cc)
add_library(part_two)
target_sources(part_two PUBLIC FILE_SET CXX_MODULES FILES part_two.cc)
add_executable(demo demo.cc)
target_link_libraries(demo input part_one part_two)
How to debug in VSCode
While you could manually configure debugging using the VSCode launch.json approach, that is not necessary with the CMake extension.
Set a breakpoint in the code and right-click on the CMake target you want to debug.
This will switch VSCode into the debugging view.
If you cannot find the panel with CMake targets, it is by default in the CMake extension panel, but you are free to move it like I did in the first screenshot.
Other tools
While the repository is configured for a very specific combination of CMake, Clang and GCC, nothing prevents you from using a different combination. However, at minimum, you will need to sacrifice the quality of code completion, and the C++20 module support in Bazel is still a work in progress.
Thank you very much for putting this together! It has been very useful. I was wondering if I could have some guidance on the following two things
- I can't seem to include the `<print>` header even though cppreference seems to suggest that it should have been implemented by the version of gcc used
- I want to experiment with third-party libraries like boost, what would be the easiest way of going about this?
Thanks for sharing the build .Please do add some examples for debugging