Go back

The Easy Guide to g++ Command Line Arguments for C++23

Published:

Easy Guide to g++ Command Line Arguments

Table of Contents

Open Table of Contents

Introduction

After almost four decades of C++ development, I’ve learned that getting your compiler flags right can make the difference between shipping a product and debugging mysterious crashes at 3 AM. Whether you’re building high-frequency trading systems that need every microsecond or game engines where a single frame drop ruins the experience, your g++ command line is your first line of defense against bugs and performance issues.

Today, I want to share what I consider the canonical g++ configurations for both debugging and release builds in the C++23 era. These aren’t just random collections of flags—each one serves a specific purpose, and understanding why you need them will make you a better systems programmer.

The Foundation: Understanding Modern g++ Flags

Before diving into specific configurations, let’s establish what we’re optimizing for. In debug builds, we prioritize developer productivity: fast compilation, excellent debugging information, and aggressive error detection. In release builds, we prioritize runtime performance while maintaining reasonable safety margins.

Here are my recommended starting configurations:

Debug Configuration:

g++ -std=c++23 -Og -march=native -ggdb -fsanitize=address,undefined -pthread -Wall -Wextra -Wpedantic

Release Configuration:

g++ -std=c++23 -O3 -march=native -pthread -Wall -Wextra -Wpedantic -flto -DNDEBUG

Let me walk you through why each of these flags matters and when you might want alternatives.

Debug Build Deep Dive

Language Standard: -std=c++23 vs -std=gnu++23

The choice between -std=c++23 and -std=gnu++23 is more significant than many developers realize. I always recommend -std=c++23 for new projects because it enforces strict ISO C++ compliance, which means your code will be more portable across different compilers and platforms.

A colleague working on embedded automotive systems learned this the hard way when their codebase, built with -std=gnu++23, failed to compile with Green Hills compiler for their safety-critical ECU. The GNU extensions they’d inadvertently used weren’t available, forcing a costly refactoring just weeks before a production deadline.

GNU Extensions You’ll Miss:

Why Standard C++23 is Better:

💡 Pro Tip: If you absolutely need GNU extensions, consider using them sparingly and wrapping them in compiler-specific #ifdef blocks. This way, you maintain awareness of portability boundaries.

Optimization Level: -Og - The Goldilocks Option

Most developers know about -O0 (no optimization) and -O2 (standard optimization), but -Og is the secret weapon for debug builds. Introduced in GCC 4.8, it provides optimizations that don’t interfere with debugging while still making your code run at reasonable speeds.

Here’s what -Og gives you:

Compare this to -O0, which produces such slow code that interactive debugging becomes painful, especially in template-heavy codebases. I once worked on a real-time graphics engine where -O0 builds ran at 2 FPS, making it impossible to debug rendering issues. Switching to -Og got us to 30 FPS while maintaining full debugging capability.

Optimization LevelDebug Info QualityRuntime SpeedUse Case
-O0ExcellentVery SlowSimple programs only
-OgExcellentModerateRecommended for debug
-O1GoodFastLight debugging
-O2PoorVery FastRelease builds

Architecture Optimization: -march=native

The -march=native flag tells GCC to optimize for your specific CPU architecture, enabling all instruction sets your processor supports. This includes SSE, AVX, AVX2, AVX-512, and other extensions that can dramatically improve performance.

Important: -march=native implies -mtune=native. You don’t need both flags. The -march flag sets both the instruction set architecture and the tuning target.

Real-World Impact: A team I know working on cryptocurrency mining software saw a 40% performance improvement simply by switching from -march=x86-64 to -march=native on their AMD EPYC servers. The AVX2 instructions made that much difference in their hash calculations.

The Portability Trade-off: The binary produced with -march=native will only run on CPUs with the same or better feature set. For distributed software, consider:

Debug Information: -ggdb

While -g produces standard DWARF debug information, -ggdb generates GDB-specific extensions that make debugging significantly more pleasant. These extensions include:

The size overhead is minimal in debug builds, and the debugging experience improvement is substantial. I’ve seen developers struggle for hours with -g when -ggdb would have shown them the exact problem immediately.

Sanitizers: Your Bug-Catching Net

-fsanitize=address,undefined

AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) are probably the most powerful debugging tools in the modern C++ developer’s arsenal. They catch bugs that traditional debugging often misses.

AddressSanitizer catches:

UndefinedBehaviorSanitizer catches:

Performance Impact: ASan typically slows down execution by 2-3x and increases memory usage by 2-3x. UBSan has minimal performance impact. This is perfectly acceptable for debug builds.

Real-World Success Story: A game studio I consulted for was experiencing random crashes in their multiplayer backend, but only under heavy load. Traditional debugging couldn’t reproduce the issue. After enabling sanitizers, they discovered a race condition causing a use-after-free bug that had been hiding for months. The fix took 30 minutes once they knew what to look for.

⚠️ Important: Never ship with sanitizers enabled in production. They’re development tools only.

Threading Support: -pthread

The -pthread flag is almost always necessary in modern C++ development, even if you think you’re not using threads. Here’s why:

  1. Standard Library Dependencies: Many C++11+ features require threading support:

    • std::thread
    • std::async
    • std::future/std::promise
    • Thread-local storage
  2. Third-Party Library Requirements: Libraries like Boost, Qt, and even some seemingly single-threaded libraries may use threading internally.

  3. Compiler Implementation Details: Some compiler optimizations and runtime features expect threading support to be available.

The flag does more than just link the pthread library—it also:

Omitting -pthread Horror Story: I once debugged a mysterious crash in a financial modeling application that only occurred on multi-core systems. The issue? They weren’t using -pthread, so exception handling wasn’t thread-safe. When exceptions were thrown from worker threads, the program would randomly crash during stack unwinding.

Warning Flags: Your Quality Gates

-Wall -Wextra -Wpedantic

These warning flags form a progressive hierarchy of code quality enforcement:

-Wall enables the most important warnings:

-Wextra adds additional useful warnings:

-Wpedantic enforces strict ISO C++ compliance:

Consider treating warnings as errors in your build system with -Werror, but be prepared for the maintenance overhead. I recommend starting with -Werror on new projects and gradually cleaning up existing codebases.

Release Build Configuration

Optimization: -O3 - Maximum Performance

For release builds, -O3 is typically the sweet spot. It enables all -O2 optimizations plus:

When NOT to Use -O3:

The -Ofast Trap

Many developers see -Ofast and think “faster is better,” but this flag can break your program’s correctness. It enables optimizations that don’t strictly conform to standards:

A quantitative finance team I worked with learned this lesson when -Ofast caused their risk calculations to produce slightly different results compared to their verified -O3 builds. In finance, even tiny differences in calculations can mean millions of dollars in trading losses.

Use -Ofast only when:

Link-Time Optimization (LTO) performs optimizations across translation units, something traditional compilation can’t do. It can provide 5-15% performance improvements by:

LTO Trade-offs:

When to Use LTO:

Add -flto to both compile and link commands:

# Compile
g++ -std=c++23 -O3 -march=native -flto -c source.cpp
# Link
g++ -std=c++23 -O3 -march=native -flto -o program objects...

The NDEBUG Macro

Adding -DNDEBUG to release builds disables assert statements throughout your code and standard library. This can provide meaningful performance improvements in assertion-heavy code.

// This code disappears in release builds with -DNDEBUG
assert(index < container.size());

Be careful—assertions often catch bugs that shouldn’t happen in production. Make sure your code handles error conditions gracefully without relying solely on assertions.

Advanced Considerations and Alternatives

Security Hardening Flags

For production systems, consider additional security flags:

# Stack protection
-fstack-protector-strong

# Format string protection
-Wformat -Wformat-security

# Position Independent Executable
-pie -fPIE

# Control Flow Integrity (GCC 8+)
-fcf-protection=full

Memory Management Alternatives

For debug builds dealing with complex memory issues, consider these alternatives to AddressSanitizer:

# Valgrind-friendly build (no sanitizers)
g++ -std=c++23 -Og -g -pthread -Wall -Wextra -Wpedantic

# Memory Sanitizer (Clang only)
clang++ -fsanitize=memory

# Thread Sanitizer (for race conditions)
g++ -fsanitize=thread

Profile-Guided Optimization

For maximum performance in critical applications, consider Profile-Guided Optimization (PGO):

# Step 1: Build with instrumentation
g++ -std=c++23 -O3 -march=native -fprofile-generate

# Step 2: Run representative workload
./program < typical_input.data

# Step 3: Build with profile data
g++ -std=c++23 -O3 -march=native -fprofile-use

PGO can provide 10-30% performance improvements in CPU-bound applications by optimizing based on actual runtime behavior.

Compiler Support and Portability

GCC Version Requirements

Different flags have different minimum GCC version requirements:

FlagMinimum GCC VersionNotes
-std=c++23GCC 11Partial support, GCC 13+ recommended
-OgGCC 4.8Stable since GCC 5
-fsanitize=addressGCC 4.8Mature since GCC 6
-fsanitize=undefinedGCC 4.9Full support in GCC 7+
-fltoGCC 4.5Significantly improved in GCC 9+

Cross-Compiler Compatibility

If you need to support multiple compilers, here’s a compatibility matrix:

Clang Equivalents: Most flags work identically with Clang++, with these exceptions:

MSVC Considerations: When porting to MSVC, you’ll need different flags:

Real-World Examples Across Industries

Game Development: Frame Time Optimization

A AAA game studio I collaborated with used these specialized flags for their engine builds:

# Debug builds for gameplay programming
g++ -std=c++23 -Og -march=native -ggdb -fsanitize=address \
    -pthread -Wall -Wextra -DGAME_DEBUG -fno-omit-frame-pointer

# Release builds for shipping
g++ -std=c++23 -O3 -march=native -pthread -Wall -Wextra \
    -flto -DNDEBUG -ffast-math -DGAME_RELEASE

Note the -ffast-math in their release builds—they could use it safely because their game logic didn’t require strict IEEE 754 compliance, and it provided a 5-8% performance boost in their physics calculations.

High-Frequency Trading: Microsecond Matters

A quantitative trading firm uses these configurations for their latency-critical systems:

# Ultra-low latency release build
g++ -std=c++23 -O3 -march=native -pthread -flto -DNDEBUG \
    -fno-exceptions -fno-rtti -static-libgcc -static-libstdc++

The -fno-exceptions and -fno-rtti flags eliminate runtime overhead that could add microseconds to trade execution. The static linking ensures predictable performance without dynamic loading delays.

Embedded Systems: Size Constraints

An IoT device manufacturer uses size-optimized builds:

# Size-optimized release
g++ -std=c++23 -Os -march=cortex-m4 -pthread -Wall -Wextra \
    -flto -DNDEBUG -ffunction-sections -fdata-sections -Wl,--gc-sections

The -Os flag optimizes for size instead of speed, crucial when fitting into microcontroller flash memory. The section-based flags enable dead code elimination at link time.

Common Pitfalls and Gotchas

1. Sanitizer Compatibility Issues

Don’t combine AddressSanitizer with ThreadSanitizer—they’re mutually exclusive and will cause build failures. Use separate debug configurations for different types of bug hunting.

2. LTO and Debug Information

Link-Time Optimization can sometimes interfere with debugging. If you need to debug optimized release builds, consider building with -flto -g but be aware that some variables may be optimized away.

3. Architecture-Specific Builds

Remember that -march=native creates binaries tied to your build machine’s CPU. A crypto startup I advised had to rebuild their entire production system when they moved from Intel to AMD servers because they forgot about this dependency.

4. Sanitizer False Positives

Sanitizers occasionally report false positives, especially when interfacing with C libraries or doing low-level memory manipulation. Learn to use suppression files for known false positives:

export ASAN_OPTIONS=suppressions=my_suppressions.txt

Building Your Own Standards

While these configurations provide excellent starting points, every project has unique requirements. I recommend:

  1. Start with these baselines and measure their impact on your specific workloads
  2. Document your choices in build scripts with comments explaining why each flag is needed
  3. Test thoroughly when changing optimization levels or adding new flags
  4. Monitor performance in both debug and release builds to catch regressions early

Sample Makefile Integration

Here’s how I typically integrate these configurations into a Makefile:

CXX = g++
COMMON_FLAGS = -std=c++23 -march=native -pthread -Wall -Wextra -Wpedantic

DEBUG_FLAGS = $(COMMON_FLAGS) -Og -ggdb -fsanitize=address,undefined -DDEBUG
RELEASE_FLAGS = $(COMMON_FLAGS) -O3 -flto -DNDEBUG

.PHONY: debug release

debug:
	$(CXX) $(DEBUG_FLAGS) -o program_debug $(SOURCES)

release:
	$(CXX) $(RELEASE_FLAGS) -o program_release $(SOURCES)

Looking Forward: The Future of g++ Flags

As C++26 approaches and processors continue evolving, keep an eye on:

The fundamentals covered here will remain relevant, but the specific flags and their defaults will continue evolving. Stay updated with GCC release notes and consider upgrading your toolchain regularly to benefit from improvements.

Conclusion

Getting your g++ command line right is one of those foundational skills that pays dividends throughout your career. These configurations represent thousands of hours of collective experience from developers across industries, distilled into practical, proven setups.

Start with the canonical configurations I’ve provided, understand why each flag matters, and then adapt them to your specific needs. Remember that the best configuration is the one that helps you ship reliable, performant software—not necessarily the one with the most flags.

Your compiler is your first collaborator in writing great code. Treat it well, and it will help you catch bugs early, optimize aggressively, and build systems that users love to use.


Have questions about specific flags or want to share your own g++ configurations? I’d love to hear from you. The best practices in this space evolve constantly, and learning from each other’s experiences makes us all better developers.


Suggest Changes

Next Post
Mastering C++ reserve(): The Performance Game-Changer You're Probably Not Using Enough