Skip to content

Storage and Attributes

New to this topic?

Understanding storage duration and linkage is crucial for writing correct C++ programs. This topic covers how variables are stored, when they're created and destroyed, and how to control their visibility across translation units.

Storage Duration

Storage duration determines when a variable is created and destroyed. C++ has four storage durations:

Automatic Storage Duration

Variables with automatic storage duration are created when they come into scope and destroyed when they go out of scope:

cpp
void function() {
    int x = 42;        // Automatic storage duration
    {
        int y = 10;    // Automatic storage duration
        // y is destroyed here
    }
    // x is destroyed here
}

Characteristics:

  • Created when declared
  • Destroyed when scope ends
  • Stored on the stack
  • Most common storage duration

Static Storage Duration

Variables with static storage duration are created when the program starts and destroyed when the program ends:

cpp
int globalVar = 42;  // Static storage duration

void function() {
    static int count = 0;  // Static local variable
    count++;
    std::cout << "Function called " << count << " times" << std::endl;
}

class MyClass {
    static int classVar;  // Static member variable
};

Characteristics:

  • Created at program startup
  • Destroyed at program termination
  • Stored in data segment
  • Zero-initialized by default

Thread-Local Storage Duration

Variables with thread-local storage duration have separate instances for each thread:

cpp
#include <thread>

thread_local int threadVar = 0;  // Each thread gets its own copy

void threadFunction() {
    threadVar++;  // Modifies this thread's copy
    std::cout << "Thread " << std::this_thread::get_id() 
              << " has value: " << threadVar << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();
    return 0;
}

Characteristics:

  • Each thread has its own copy
  • Created when thread starts
  • Destroyed when thread ends
  • Useful for thread-specific data

Dynamic Storage Duration

Variables with dynamic storage duration are manually managed with new and delete:

cpp
int* ptr = new int(42);  // Dynamic storage duration
// ... use ptr ...
delete ptr;  // Manual destruction

Characteristics:

  • Manual allocation/deallocation using new/delete
  • Useful for data that needs to outlive the function that created it
  • Can be used to allocate memory on the heap
  • User needs to take care of lifecycle management

malloc/free

C only supports malloc and free for dynamic memory allocation, while C++ introduced new and delete as a safer and more feature-rich alternative. In modern C++, you should almost always prefer new/delete over malloc/free because:

  • new/delete automatically call the object's constructor and destructor, respectively; malloc/free do not
  • new is type-safe --- it returns a pointer of the correct type. malloc, on the other hand, returns a void*, which requires an explicit cast in C++

Linkage

Linkage determines how names are shared across translation units:

External Linkage

Names with external linkage can be used across translation units:

cpp
// file1.cpp
int globalVar = 42;  // External linkage
void function() {}    // External linkage

// file2.cpp
extern int globalVar;  // Declaration, not definition
extern void function(); // Declaration, not definition

Internal Linkage

Names with internal linkage are only visible within the current translation unit:

cpp
static int fileVar = 42;  // Internal linkage
static void fileFunction() {}  // Internal linkage

namespace {
    int anonymousVar = 10;  // Internal linkage (anonymous namespace)
}

No Linkage

Names with no linkage are only visible within their scope:

cpp
void function() {
    int localVar = 42;  // No linkage
    class LocalClass {}; // No linkage
}

The static Keyword

The static keyword has different meanings depending on context:

Static Local Variables

cpp
void counter() {
    static int count = 0;  // Initialized once, persists between calls
    count++;
    std::cout << "Called " << count << " times" << std::endl;
}

int main() {
    counter();  // Called 1 times
    counter();  // Called 2 times
    counter();  // Called 3 times
    return 0;
}

Static Member Variables

cpp
class Counter {
public:
    static int totalCount;  // Shared across all instances of the same class
    int instanceCount;

    Counter() : instanceCount(0) {
        totalCount++;
    }
};

int Counter::totalCount = 0;  // Definition (required)

int main() {
    Counter c1, c2, c3;
    std::cout << "Total instances: " << Counter::totalCount << std::endl;  // 3
    return 0;
}

Static Member Functions

cpp
class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }

    static double PI;  // Static member variable
};

double MathUtils::PI = 3.14159;

int main() {
    int result = MathUtils::add(5, 3);  // Call without instance
    std::cout << "PI: " << MathUtils::PI << std::endl;
    return 0;
}

The inline Keyword

The inline keyword serves two purposes:

1. ODR (One Definition Rule) Compliance

cpp
// header.h
inline int add(int a, int b) {
    return a + b;
}

inline int globalValue = 42;

// file1.cpp
#include "header.h"
// Can use add() and globalValue

// file2.cpp
#include "header.h"
// Can also use add() and globalValue
// No multiple definition error!

2. Optimization Hint

cpp
inline int square(int x) {
    return x * x;  // Compiler may inline this function
}

int main() {
    int result = square(5);  // May be optimized to: int result = 25;
    return 0;
}

Important: The compiler may ignore the inline hint. Modern compilers make their own inlining decisions.

const and constexpr

const

const indicates that a value cannot be modified after initialization:

cpp
const int MAX_SIZE = 100;  // Runtime constant
const int* ptr = &MAX_SIZE;  // Pointer to const int
int* const constPtr = nullptr;  // Const pointer to int
const int* const constConstPtr = &MAX_SIZE;  // Const pointer to const int

void function(const int& param) {
    // param cannot be modified
}

constexpr

constexpr indicates that a value must be computable at compile time:

cpp
constexpr int ARRAY_SIZE = 100;  // Compile-time constant
constexpr double PI = 3.14159;

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

int array[factorial(5)];  // OK: factorial(5) is computed at compile time

Key differences:

  • const can be runtime or compile-time
  • constexpr must be compile-time
  • constexpr enables more optimizations

C++ Attributes

Attributes provide additional information to the compiler:

[[nodiscard]]

Generates a warning if the return value is ignored:

cpp
[[nodiscard]] int createResource() {
    return 42;
}

int main() {
    createResource();  // Warning: return value ignored
    int result = createResource();  // OK
    return 0;
}

[[likely]] and [[unlikely]]

Hints to the compiler about branch prediction:

cpp
int processValue(int value) {
    if (value > 0) [[likely]] {
        return value * 2;
    } else [[unlikely]] {
        return 0;
    }
}

[[maybe_unused]]

Suppresses warnings about unused variables:

cpp
[[maybe_unused]] int debugValue = 42;

void function([[maybe_unused]] int param) {
    // param might not be used in all configurations
}

[[deprecated]]

Marks code as deprecated:

cpp
[[deprecated("Use newFunction instead")]]
void oldFunction() {
    // Old implementation
}

void newFunction() {
    // New implementation
}

Common Pitfalls

1. Multiple Definitions Without inline

cpp
// header.h
int add(int a, int b) {  // No inline!
    return a + b;
}

// file1.cpp
#include "header.h"
// Definition of add

// file2.cpp
#include "header.h"
// Another definition of add - LINKER ERROR!

Fix: Use inline or move definition to source file:

cpp
// header.h
inline int add(int a, int b) {
    return a + b;
}

2. Static Local Initialization Order

cpp
int getValue() {
    static int value = computeValue();  // When does this execute?
    return value;
}

int computeValue() {
    // Complex computation
    return 42;
}

Problem: The order of static initialization is not guaranteed across translation units. Fix: Use function-local static variables or explicit initialization:

cpp
int getValue() {
    static int value = () {
        return computeValue();
    }();
    return value;
}

3. Const vs Constexpr Confusion

cpp
const int runtimeValue = getRuntimeValue();  // OK
constexpr int compileTimeValue = 42;         // OK
constexpr int badValue = getRuntimeValue();  // ERROR!

4. Thread-Local Misuse

cpp
thread_local int sharedData = 0;  // Each thread has its own copy

void function() {
    sharedData++;  // Only affects current thread
    // Don't expect other threads to see this change
}

Best Practices

1. Use constexpr for Compile-Time Constants

cpp
constexpr int BUFFER_SIZE = 1024;
constexpr double GRAVITY = 9.81;

// Instead of:
// const int BUFFER_SIZE = 1024;

2. Use inline for Header-Only Functions

cpp
// header.h
inline int clamp(int value, int min, int max) {
    return (value < min) ? min : (value > max) ? max : value;
}

3. Use [[nodiscard]] for Error-Checking Functions

cpp
[[nodiscard]] bool initializeSystem() {
    // Return true if successful, false otherwise
    return true;
}

4. Use Static Local Variables for Lazy Initialization

cpp
const std::string& getConfig() {
    static const std::string config = loadConfigFromFile();
    return config;
}

5. Use Anonymous Namespaces for Internal Linkage

cpp
namespace {
    int internalHelper() {
        return 42;
    }
}

Example: Storage Duration Demo

cpp
#include <iostream>
#include <thread>

// Static storage duration (external linkage)
int globalCounter = 0;

// Thread-local storage duration
thread_local int threadCounter = 0;

class StorageDemo {
private:
    // Static member variable
    static int classCounter;

    // Instance variable (automatic when object is automatic)
    int instanceCounter;

public:
    StorageDemo() : instanceCounter(0) {
        classCounter++;
        threadCounter++;
    }

    // Static member function
    static int getClassCounter() {
        return classCounter;
    }

    void increment() {
        instanceCounter++;
        globalCounter++;
    }

    void printCounters() {
        std::cout << "Instance: " << instanceCounter << std::endl;
        std::cout << "Class: " << classCounter << std::endl;
        std::cout << "Global: " << globalCounter << std::endl;
        std::cout << "Thread: " << threadCounter << std::endl;
    }
};

// Definition of static member
int StorageDemo::classCounter = 0;

// Function with static local variable
void functionWithStatic() {
    static int callCount = 0;
    callCount++;
    std::cout << "Function called " << callCount << " times" << std::endl;
}

int main() {
    // Automatic storage duration
    StorageDemo demo1, demo2;

    demo1.increment();
    demo2.increment();

    demo1.printCounters();
    // Instance: 1
    // Class: 2
    // Global: 2
    // Thread: 2 

    functionWithStatic();    // Function called 1 times 
    functionWithStatic();    // Function called 2 times

    return 0;
}

Summary

You've learned:

  • Different storage durations and when to use each
  • How linkage affects name visibility
  • The multiple meanings of the static keyword
  • How inline helps with ODR compliance
  • The difference between const and constexpr
  • Modern C++ attributes and their uses
  • Common pitfalls and how to avoid them

Key takeaways:

  • Choose the right storage duration for your use case
  • Use inline for header-only functions
  • Use constexpr for compile-time constants
  • Use attributes to provide hints to the compiler
  • Understand the difference between const and constexpr
  • Be careful with static initialization order

Remember: Understanding storage and linkage is essential for writing correct, efficient C++ programs!

Questions

Q: What is the difference between automatic and static storage duration?

Automatic storage duration means variables are created when they come into scope and destroyed when they go out of scope. Static storage duration means variables are created when the program starts and destroyed when the program ends, regardless of scope.

Q: What does the inline keyword do for functions?

The inline keyword allows multiple definitions of the same function across different translation units, which helps satisfy the One Definition Rule (ODR). It's a hint to the compiler for optimization, but the compiler may ignore it.

Q: What is the purpose of the [[nodiscard]] attribute?

The [[nodiscard]] attribute generates a compiler warning if the return value of a function is ignored. This is useful for functions where ignoring the return value likely indicates a bug, such as error-checking functions.

Q: What is the difference between const and constexpr?

constexpr indicates that a value must be computable at compile time, while const only indicates that a value cannot be modified after initialization. constexpr is more restrictive but enables compile-time optimization.

Q: What happens if you have multiple definitions of a non-inline function?

Having multiple definitions of a non-inline function across translation units violates the One Definition Rule (ODR) and results in a linker error. The linker cannot determine which definition to use.