VS Code C++ Setup: IntelliSense, Build Tasks, and Debugging

[C++ Hands-On Guide #3] VS Code C++ Configuration

VS Code is not a C++ IDE out of the box. It is an editor that, with Microsoft’s C/C++ extension, talks to three separate things you install yourself: a compiler (g++, clang++ or MSVC), a debugger (GDB, LLDB or the Visual Studio debugger), and optionally a build system such as CMake. Each connection has its own configuration file in .vscode/, and most setup problems come from those files disagreeing with each other or with the command line.

“The terminal builds, but the editor is full of red squiggles”

The C++ extension needs to know which compiler you use and where system headers live. If compilerPath is wrong, <iostream> / <vector> may show errors even when g++ succeeds. IntelliSense = completion, signature help, and error squiggles while typing.

This is the single most common confusion, and it happens because IntelliSense is not your compiler. The extension runs its own parser (derived from the EDG front end) and only borrows information from the compiler at compilerPath: it queries it for its built-in include directories and predefined macros. A successful terminal build therefore proves nothing about IntelliSense, and a clean editor proves nothing about the build. When the two disagree, fix the configuration, not the code.

Scenario quick hits

  1. New teammate, new PC: Commit .vscode/ with OS-specific c_cpp_properties.json variants or document compilerPath per OS.
  2. CMake project, IntelliSense broken: Generate compile_commands.json (CMAKE_EXPORT_COMPILE_COMMANDS) or configure CMake Tools.
  3. WSL builds: Open the folder in WSL (Remote - WSL); point compilerPath to /usr/bin/g++.
  4. preLaunchTask not found: tasks.json label must exactly match launch.json preLaunchTask.
  5. macOS LLDB errors: Use MIMode: lldb; install Xcode CLI tools. Prerequisites: A working compiler (#2) and optionally CMake (#4).
flowchart TB
    subgraph editor[VS Code]
        A[Edit sources]
        B[IntelliSense]
    end
    subgraph config[.vscode/]
        C[c_cpp_properties.json]
        D[tasks.json]
        E[launch.json]
    end
    subgraph run[Build & run]
        F[g++/clang++]
        G[Binary]
        H[GDB/LLDB]
    end
    C --> B
    D --> F
    F --> G
    E --> H
    H --> G

Install VS Code

Download from code.visualstudio.com. Enable Add to PATH so code . works from a terminal. Install the C/C++ extension (Microsoft). Optional: CMake Tools, C/C++ Extension Pack.


IntelliSense — c_cpp_properties.json

Create .vscode/c_cpp_properties.json:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": ["${workspaceFolder}/**"],
            "defines": [],
            "compilerPath": "/usr/bin/g++",
            "cStandard": "c17",
            "cppStandard": "c++17",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

Windows (MinGW) example:

"compilerPath": "C:/msys64/mingw64/bin/g++.exe",
"intelliSenseMode": "windows-gcc-x64"

macOS (Clang):

"compilerPath": "/usr/bin/clang++",
"intelliSenseMode": "macos-clang-x64"

Run which g++ / where g++ and paste the exact path into compilerPath.

What each field actually controls:

  • compilerPath is the most important line. The extension runs this compiler once to discover system include paths and built-in macros such as __GNUC__ or _WIN32. You do not need to list /usr/include/c++/13 in includePath; if you find yourself doing that, compilerPath is wrong.
  • includePath is only for your headers and third-party libraries. ${workspaceFolder}/** makes the extension search every subfolder recursively, which is convenient for small projects and slow on big ones (it also picks up headers from build directories and vendored copies, so two same-named headers can resolve to the wrong one).
  • defines should mirror the -D flags from your build. If the build uses -DUSE_SSL and IntelliSense does not, #ifdef USE_SSL blocks appear greyed out and completion inside them stops working.
  • cppStandard must match -std= in the build task, or IntelliSense flags valid C++20 code as errors (or accepts code the compiler rejects).
  • intelliSenseMode tells the parser which compiler’s quirks and type sizes to emulate. A msvc-x64 mode with a MinGW g++.exe is a classic mismatch that produces odd errors around long sizes and compiler extensions.

The quickest check is the command palette entry C/C++: Log Diagnostics, which prints the include paths and defines IntelliSense is really using for the current file. When squiggles do not make sense, I read that output before touching anything else; it usually shows either the wrong compiler or a missing define within seconds.


Build tasks — tasks.json

Default build for the current file:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "C++ Build",
            "type": "shell",
            "command": "g++",
            "args": [
                "-g",
                "-std=c++17",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "group": { "kind": "build", "isDefault": true }
        }
    ]
}

Ctrl+Shift+B (mac: Cmd+Shift+B) runs the default build task. For multiple files, list them explicitly or switch to CMake.

${file} is the file in the active editor tab, so this task compiles only that file. That is the right shape for single-file exercises and wrong for anything with more than one .cpp: pressing Ctrl+Shift+B while utils.cpp is open tries to build utils.cpp alone and fails with undefined reference to 'main'. Two additions make the task much more useful. Add -Wall -Wextra to the arguments, because VS Code shows compiler warnings only if the compiler emits them. And add "problemMatcher": ["$gcc"], which parses the compiler output so errors appear in the Problems panel with clickable locations instead of only as text in the terminal.

"type": "shell" runs the command through your shell, so shell quoting rules apply to the arguments; "type": "process" runs the executable directly and avoids quoting surprises with paths that contain spaces.


Debugging — launch.json

Linux example (GDB):

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "C++ Debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "cwd": "${fileDirname}",
            "MIMode": "gdb",
            "miDebuggerPath": "/usr/bin/gdb",
            "preLaunchTask": "C++ Build"
        }
    ]
}

macOS: "MIMode": "lldb" (often omit miDebuggerPath).
Windows MinGW: point miDebuggerPath to gdb.exe. F5 starts debugging; ensure preLaunchTask matches tasks.json label.

"type": "cppdbg" means the extension drives GDB or LLDB through the machine interface (MI), which is why MIMode and miDebuggerPath exist. On Windows with MSVC you use "type": "cppvsdbg" instead, which talks to the Visual Studio debugger engine and ignores the MI settings. program must point at the binary the build task produces; if the two paths drift apart, F5 happily launches an old binary and breakpoints stop matching the source.

The -g flag in the build task is what makes debugging possible at all: without it the binary has no line tables and breakpoints show as hollow circles (“unverified”). Keep optimization at -O0 (the default without -O) while stepping; at -O2 the compiler reorders and removes code, so execution jumps around and many variables show <optimized out>. Two other settings are worth knowing: "externalConsole": true gives programs that read from std::cin a real console window, and "stopAtEntry": true pauses at main so you can set breakpoints before anything runs.

On macOS, cppdbg relies on an lldb-mi bridge, and that bridge has historically been the fragile part of the setup (launch failures after Xcode updates, early lack of native Apple Silicon support). If it gives you trouble, the CodeLLDB extension ("type": "lldb") talks to LLDB directly and is a common alternative.


Common issues

SymptomFix
Red squiggles on standard headersFix compilerPath + intelliSenseMode
Header not foundAdd paths to includePath
preLaunchTask not foundMatch strings exactly
C++20 features flaggedSet cppStandard to c++20 and add -std=c++20 to tasks
undefined reference in tasks buildAdd all .cpp files or use CMake
Variables “optimized out”Build with -O0 -g for debugging

Reset IntelliSense: Command Palette → C/C++: Reset IntelliSense Database.

Most rows in this table are the same underlying problem seen from different angles: one of the three files disagrees with the actual build. undefined reference is a linker error from the build task, not an IntelliSense problem, so no change to c_cpp_properties.json will fix it. Conversely, red squiggles with a successful build are never fixed by changing tasks.json. Deciding first which of the two tools is complaining saves a lot of random edits.

On Windows, a frequent variant is having more than one toolchain installed (an old MinGW, MSYS2’s UCRT64 and MinGW64 environments, and MSVC). where g++ may list several, the terminal uses the first one on PATH, and compilerPath points at another. The program then builds with one standard library and IntelliSense parses against a different one; keep a single toolchain on PATH or use absolute paths in both files.


Worked example (multi-file)

main.cpp, utils.h, utils.cpp — use a second task:

{
    "label": "C++ Build Multi-File",
    "type": "shell",
    "command": "g++",
    "args": ["-g", "-std=c++17", "main.cpp", "utils.cpp", "-o", "myapp"],
    "options": { "cwd": "${workspaceFolder}" },
    "group": "build"
}

Point launch.json program to ${workspaceFolder}/myapp and preLaunchTask to that label. CMake: Install CMake Tools → Configure → Build (F7) → debug with the generated targets.

Listing files by hand works up to a handful of sources, but it recompiles every file on every build and you must remember to add new files in two places. That is the point where I stop maintaining tasks.json and switch to CMake: CMake Tools builds only what changed, launches the selected target under the debugger without a launch.json (via CMake: Debug), and exports compile_commands.json so IntelliSense sees exactly the flags each file is compiled with. Once that file exists, set "configurationProvider": "ms-vscode.cmake-tools" in c_cpp_properties.json (or in settings), and the includePath/defines lists stop mattering.


Productivity

  • Ctrl+Shift+B — build
  • F5 — debug
  • Ctrl+P — quick open
  • Ctrl+Shift+P — command palette

Production patterns

  • Commit .vscode/ for shared team settings (exclude only personal noise if needed).
  • Align tasks.json commands with CI (g++ ... lines) to avoid “works locally, fails in CI”.
  • For large projects, prefer compile_commands.json from CMake instead of hand-maintained includePath.

Committing .vscode/ is a trade-off. tasks.json and launch.json are usually portable if they use variables like ${workspaceFolder}, but c_cpp_properties.json with an absolute compilerPath works on one machine only. A common arrangement is to commit tasks and launch configurations, keep compiler paths out of shared files by relying on compile_commands.json, and let each developer’s user settings supply the rest.


Checklist

  • C/C++ extension installed
  • compilerPath matches which g++ / clang++
  • cppStandard matches project
  • Build task works (Ctrl+Shift+B)
  • Debug works (F5) with matching preLaunchTask

Next: CMake intro (#4)

Previous: Compiler basics (#2)