Install a C++ toolchain on Windows

Windows has no compiler of its own, so the first decision is which one to install. Three setups give a complete C++ toolchain, and each one has a different compiler underneath: MSYS2 with MinGW-w64, Visual Studio Build Tools, and WSL with a Linux distribution.

This lesson compares them, installs the two native ones step by step, and builds the same source with the flags of each compiler. Every program here except windows_only.cpp was compiled and run on the Linux machine described at the end of the page; windows_only.cpp stops at its first include, as its section shows. The Windows commands are the same words with .exe at the end, and neither MSYS2 nor cl.exe was run for this page.

Three ways to get a compiler

MSYS2 with MinGW-w64 installs GCC, the compiler this course uses on Linux, so every warning flag and every -std value in the course works unchanged. Visual Studio Build Tools installs MSVC, the compiler behind Visual Studio and most Windows binaries. WSL runs a Linux distribution inside Windows and gives the Linux toolchain of lesson 001.

All three compile the same C++, and the preprocessor says which one is at work. The macros _WIN32, __GNUC__ and _MSC_VER are set by the compiler itself, so a program can report its own toolchain without a build script.

#include <iostream> int main() { #if defined(_WIN32) std::cout << "target: Windows\n"; #else std::cout << "target: not Windows\n"; #endif #if defined(__GNUC__) std::cout << "compiler: GCC " << __VERSION__ << '\n'; #elif defined(_MSC_VER) std::cout << "compiler: MSVC " << _MSC_VER << '\n'; #endif std::cout << "standard: " << __cplusplus << '\n'; }

g++ -std=c++23 -Wall -Wextra identity.cpp -o identity
./identity

target: not Windows
compiler: GCC 15.2.0
standard: 202302

MSYS2 and MinGW-w64

MSYS2 is a package manager and a Unix shell for Windows. Install it from msys2.org, open the UCRT64 shell it adds to the Start menu, and run pacman -S mingw-w64-ucrt-x86_64-gcc. The package places g++.exe in the ucrt64/bin folder of the MSYS2 install, and Windows finds it from any directory once that folder is on PATH.

The source is ordinary C++ and knows nothing about Windows. The commands below are the ones this lesson ran on Linux; on Windows they are the same words, and the output file is hello.exe.

Keep the UCRT64 shell for building: it sets PATH for that session, which is the quickest way to try the compiler before touching the system environment variables.

#include <iostream> #include <string> int main() { std::string target = "a C++ compiler"; std::cout << "Hello from " << target << "!\n"; }

g++ -std=c++23 -Wall -Wextra hello.cpp -o hello
./hello

Hello from a C++ compiler!

Visual Studio Build Tools

The Build Tools are a smaller install than the full IDE: the installer asks for the Desktop development with C++ workload, and that brings cl.exe, the Windows SDK and the linker. Visual Studio itself is optional; the compiler is the part that matters here.

cl.exe is not on PATH by default. The Developer Command Prompt for VS opens a console where it is, and the same prompt sets the environment variables the linker needs, which is why a plain cmd window often fails where the developer prompt works.

The flag names differ from GCC: /std:c++latest or /std:c++20 instead of -std, /W4 instead of -Wall -Wextra, and /EHsc for standard exception handling. The program below builds with g++ here and uses nothing that cl.exe rejects; the flags above are the ones cl takes.

#include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> words{"portable", "source", "builds", "twice"}; for (std::size_t i = 0; i < words.size(); ++i) { if (i > 0) { std::cout << ' '; } std::cout << words[i]; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra portable.cpp -o portable
./portable

portable source builds twice

The two toolchains disagree on sizes

Linux on x86_64 uses the LP64 model: long and pointers are 8 bytes wide. Windows uses LLP64: pointers are 8 bytes, long stays at 4, and wchar_t is 2 bytes because it holds UTF-16 code units. A program that stores a file size in a long compiles on both systems and behaves differently on one of them.

The program prints the sizes of the machine it runs on. Run it on Windows too: the pointer size matches, the long size does not. That difference is the reason the fixed-width types int32_t and int64_t from <cstdint> exist, and why the standard library returns std::size_t and std::ptrdiff_t instead of int.

#include <cstddef> #include <iostream> int main() { std::cout << "pointer: " << sizeof(void*) << " bytes\n"; std::cout << "long: " << sizeof(long) << " bytes\n"; std::cout << "wchar_t: " << sizeof(wchar_t) << " bytes\n"; std::cout << "size_t: " << sizeof(std::size_t) << " bytes\n"; }

g++ -std=c++23 -Wall -Wextra sizes.cpp -o sizes
./sizes

pointer: 8 bytes
long: 8 bytes
wchar_t: 4 bytes
size_t: 8 bytes

Verify the install

Before writing real code, check that the install compiles and runs. The program uses the standard library the way the course does: std::vector, std::string and a sorting algorithm. If it prints the four words in order, the compiler, the headers, the standard library and the linker agree.

The same file builds with g++ -Wall -Wextra and with cl /W4 /EHsc. The commands below are the g++ ones that ran here. A missing header at this point means the package is incomplete, not that the code is wrong, and reinstalling the workload is faster than editing the source.

Once this program runs, the rest of the course works the same way on Windows: save a .cpp file, compile it with the flags of your compiler, run the .exe.

#include <algorithm> #include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> parts{"build", "tools", "are", "ready"}; std::sort(parts.begin(), parts.end()); for (std::size_t i = 0; i < parts.size(); ++i) { std::cout << parts[i]; if (i + 1 < parts.size()) { std::cout << ' '; } } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra verify.cpp -o verify
./verify

are build ready tools

Code that only builds on Windows

The first surprise when a project moves between the two toolchains is a header that exists on one of them. <windows.h> is part of the Windows SDK: it declares the Win32 API and it cannot be found on Linux, so a source that includes it stops at the first line.

The message below is what the same file prints here. The fix depends on the code: a portable project hides the Windows calls behind #ifdef _WIN32 and provides a second implementation, or keeps them in a file that is only compiled on Windows.

The mirror image is a Linux-only header, such as <unistd.h>, in a file compiled with cl.exe. Both compilers say the same thing in different words: the file is not there.

#include <iostream> #include <windows.h> int main() { std::cout << "this builds only with the Windows SDK\n"; }

g++ -std=c++23 -Wall -Wextra windows_only.cpp -o windows_only

windows_only.cpp:2:10: fatal error: windows.h: No such file or directory
2 | #include <windows.h>
| ^~~~~~~~~~~
compilation terminated.

Exercises

  1. Build identity.cpp with -std=c++17 and with -std=c++23. Which line changes, and why?
  2. Add an #elif branch for clang and check the macro it defines. Which macro would you test first?
  3. Compile hello.cpp with -o hello.exe on Linux. Does the file run, and what does the extension change?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Which compiler does the MSYS2 mingw-w64 package provide?

    Answer

    GCC, driven as g++ from the MSYS2 shell

    MinGW-w64 is a port of GCC and its binutils, and the package installs g++.

  2. Why can a library built with MSVC not simply be linked into a MinGW program?

    Answer

    The two toolchains use different ABIs for names and layouts

    Name mangling, exception handling and the standard library differ, so the object formats are not interchangeable.

  3. What does the WSL option give a Windows user?

    Answer

    A Linux distribution with a Linux toolchain inside Windows

    WSL runs a real Linux user space, so the commands of the Linux lessons work unchanged.

  4. Why does this program test _WIN32? #include <iostream> int main() { #if defined(_WIN32) std::cout << "target: Windows\n"; #else std::cout << "target: not Windows\n"; #endif #if defined(__GNUC__) std::cout << "compiler: GCC " << __VERSION__ << '\n'; #elif defined(_MSC_VER) std::cout << "compiler: MSVC " << _MSC_VER << '\n'; #endif std::cout << "standard: " << __cplusplus << '\n'; }

    Answer

    To show which platform the build is running on

    The macro is defined by the compiler on Windows, so the printed line says where the binary was built.

  5. What does this program print in this container? #include <iostream> int main() { #ifdef _WIN32 std::cout << "target: Windows\n"; #else std::cout << "target: not Windows\n"; #endif }

    Answer

    target: not Windows

    The lesson runs on Linux, where _WIN32 is not defined, so the else branch is compiled.

  6. What does this program print? #include <iostream> int main() { std::cout << "wchar_t: " << sizeof(wchar_t) << " bytes\n"; }

    Answer

    wchar_t: 4 bytes

    The program reports the size the ABI gives wchar_t on this build, four bytes.

  7. What does this program print on this build? #include <iostream> int main() { std::cout << sizeof(long) << ' ' << sizeof(int) << '\n'; }

    Answer

    8 4

    The 64-bit ABI gives long eight bytes and int four, and the program prints the two sizes.

  8. What does this program print? #include <iostream> int main() { #ifdef _WIN32 std::cout << "windows"; #else std::cout << "linux"; #endif }

    Answer

    linux

    _WIN32 is defined by the compiler on Windows; this build runs on Linux, so the else branch is compiled.

  9. What does including <windows.h> produce in this container?

    Answer

    fatal error: windows.h: No such file or directory

    The header belongs to the Windows SDK, and the Linux toolchain does not ship it.

  10. Which pair of flags does the lesson use for MSVC's warning level and standard?

    Answer

    /W4 and /std:c++latest

    MSVC spells its options with a slash, and its standard switch has no c++23 value in the versions the lesson names.

All the programs on this page except windows_only.cpp were built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. windows_only.cpp was compiled there too and stopped at its first line, which is the error its section shows. MSYS2, MinGW-w64, cl.exe and Visual Studio Build Tools were not run: they are Windows-only and this page was written on Linux, so the Windows commands and flags here are documentation, not transcripts.

Article author: Arthur Isaev

Banner image