Using Weak Symbols for Default Implementations in C++

This article provides an overview of weak symbols in C++, explaining their role in the linking process and how they enable developers to supply default, overridable function implementations across translation units. You will learn the mechanics behind strong versus weak symbol resolution, how to declare weak symbols using compiler attributes, practical code patterns for fallback functionality, and important portability considerations across different toolchains.

Understanding Strong vs. Weak Symbols

During compilation, every non-static function and global variable is converted into a symbol within an object file. By default, the linker treats these as strong symbols. According to standard linking rules:

  1. Multiple Strong Symbols: If two translation units define the same strong symbol with external linkage, the linker halts with a "multiple definition" error.
  2. Strong and Weak Symbols: If a strong symbol shares the same name as one or more weak symbols, the linker discards the weak symbols and links to the strong symbol.
  3. Multiple Weak Symbols: If only weak symbols exist with the same identifier, the linker arbitrarily selects one without generating an error.

A weak symbol is a symbol marked with a specific directive informing the linker that its definition is non-authoritative and can be replaced if a stronger definition is supplied elsewhere.

Declaring Weak Symbols in C++

Standard C++ does not have a native weak keyword, but major compilers like GCC and Clang provide the __attribute__((weak)) extension for ELF-based targets:

// Declaring a weak function definition
extern "C" void system_logger(const char* message) __attribute__((weak));

extern "C" void system_logger(const char* message) {
    // Default implementation
    std::printf("[DEFAULT LOG]: %s\n", message);
}

When using C++, name mangling must be considered. If a weak symbol is intended to be overridden in another translation unit, both the default definition and the override must have matching signatures and linkage specifications (such as extern "C" or identical C++ namespaces and types) so the generated mangled symbols match exactly.

Providing Default Implementations Across Translation Units

Weak symbols are frequently used in embedded systems, systems programming, and extensible libraries to define fallback hooks.

1. The Default Implementation (logger_default.cpp)

#include <iostream>

__attribute__((weak)) void custom_log(const std::string& msg) {
    std::cout << "Fallback Logger: " << msg << std::endl;
}

2. The Header File (logger.h)

#pragma once
#include <string>

void custom_log(const std::string& msg);

3. User Code without Override

If the project compiles logger_default.cpp and main.cpp, the linker resolves calls to custom_log using the weak definition in logger_default.cpp.

// main.cpp
#include "logger.h"

int main() {
    custom_log("Application started"); // Outputs: "Fallback Logger: Application started"
    return 0;
}

4. User Code with Override (logger_override.cpp)

If a separate translation unit provides a standard, strong implementation of the same function, the linker swaps out the weak implementation automatically.

// logger_override.cpp
#include <iostream>
#include "logger.h"

// Strong symbol (no weak attribute)
void custom_log(const std::string& msg) {
    std::cerr << "[CRITICAL OVERRIDE]: " << msg << std::endl;
}

When logger_override.cpp is linked into the final binary alongside logger_default.cpp and main.cpp, all references resolve to the strong version, discarding the weak version entirely without causing a duplicate symbol error.

Weak Symbols Behind the Scenes in C++

Even when not explicitly declared via compiler attributes, the C++ compiler internally relies on weak symbols to satisfy the One Definition Rule (ODR) across translation units for:

  • Inline Functions: When an inline function is defined across multiple headers, the compiler marks each instance as a weak symbol so the linker collapses them into a single address.
  • Template Instantiations: Identical template instantiations across separate object files are emitted as weak symbols, allowing the linker to eliminate duplicates.

Toolchain and Portability Considerations

  • GCC and Clang: Support __attribute__((weak)) natively on ELF targets (Linux, BSD) and with certain constraints on Mach-O (macOS).
  • MSVC (Windows): Microsoft Visual C++ does not support __attribute__((weak)). Instead, MSVC provides #pragma comment(linker, "/alternatename:symbol=fallback_symbol"), which redirects unresolved external symbols to a fallback symbol.
  • Static Libraries: When linking static archives (.a), linkers only extract object files needed to resolve undefined symbols. If a weak implementation is in an object file inside a static library, the linker might not pull it into the final executable unless explicitly referenced.