Appearance
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 destructionCharacteristics:
- 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 definitionInternal 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 timeKey differences:
constcan be runtime or compile-timeconstexprmust be compile-timeconstexprenables 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
statickeyword - How
inlinehelps with ODR compliance - The difference between
constandconstexpr - 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
inlinefor header-only functions - Use
constexprfor compile-time constants - Use attributes to provide hints to the compiler
- Understand the difference between
constandconstexpr - 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.