C++ Storage Classes
Storage Classes and Specifiers in C++
Section titled “Storage Classes and Specifiers in C++”Storage class specifiers in C++ define the lifetime (duration) and linkage (visibility across translation units) of variables and functions. They precede the type they modify. The main storage class specifiers are:
auto(since C++11: type deduction, not storage class)register(deprecated)staticexternmutablethread_local(since C++11)
The auto Keyword (C++11 and later)
Section titled “The auto Keyword (C++11 and later)”In modern C++ (C++11 onwards), auto is not a storage class specifier. Instead, it’s used for automatic type deduction. The compiler deduces the type of the variable from its initializer. Variables declared with auto without other specifiers have automatic storage duration by default (like local variables).
// C++11 and later#include <vector>
int main() { auto count = 10; // count is deduced as int auto pi = 3.14159; // pi is deduced as double auto message = "Hello"; // message is deduced as const char*
std::vector<int> numbers = {1, 2, 3}; auto it = numbers.begin(); // it is deduced as std::vector<int>::iterator
// auto can be combined with const, &, &&, * const auto MAX_SIZE = 100; // const int auto& ref_count = count; // int&}auto simplifies code, especially with complex types like iterators or lambda expressions. The original meaning of auto (explicitly declaring automatic storage duration) is obsolete.
The register Keyword (Deprecated)
Section titled “The register Keyword (Deprecated)”The register keyword was historically a hint to the compiler to store a variable in a CPU register for faster access. However, modern compilers are excellent at optimization and register allocation, making this keyword largely obsolete and ineffective. It is deprecated in C++17 and its use is strongly discouraged. Compilers typically ignore it.
// Avoid using 'register'// register int counter; // Modern compilers ignore this hintRely on compiler optimizations rather than using register.
The static Specifier
Section titled “The static Specifier”The static keyword has different meanings depending on the context:
- Inside a function/block (Static Local Variables): The variable retains its value between function calls or block entries. It is initialized only once (thread-safe initialization since C++11). It has static storage duration but block scope.
- At namespace scope (Global Variables/Functions): It gives the variable or function internal linkage. This means it’s only visible within the current translation unit (the
.cppfile where it’s defined). This helps avoid naming conflicts between files. - Inside a class definition (Static Class Members): Creates a member variable or function that belongs to the class itself, not to any specific object instance. All objects of the class share the same static member variable. Static member functions can be called using the class name (
ClassName::staticFunc();) and can only access other static members.
Example demonstrating static local and global variables:
#include <iostream>
// Static global variable (internal linkage)// Visible only within this file (cpp_storage_classes.cpp)static int file_scope_count = 10;
// Function declarationvoid func();
int main() { while (file_scope_count > 0) { func(); file_scope_count--; // Modify file-scope variable } return 0;}
// Function definitionvoid func() { // Static local variable // Initialized only once, retains value between calls static int i = 5; i++; std::cout << "i is " << i; std::cout << " and file_scope_count is " << file_scope_count << std::endl;}When compiled and executed, this produces:
i is 6 and file_scope_count is 10i is 7 and file_scope_count is 9i is 8 and file_scope_count is 8i is 9 and file_scope_count is 7i is 10 and file_scope_count is 6i is 11 and file_scope_count is 5i is 12 and file_scope_count is 4i is 13 and file_scope_count is 3i is 14 and file_scope_count is 2i is 15 and file_scope_count is 1The extern Specifier
Section titled “The extern Specifier”The extern keyword is used to declare a variable or function that is defined in another translation unit (another .cpp file) or later in the same file. It primarily affects linkage.
- For Variables: It declares a variable without defining it (no memory is allocated). It tells the compiler that a variable with this name and type exists elsewhere (usually in another
.cppfile) and has external linkage (the default for global variables not declaredstatic). You cannot initialize a variable in anexterndeclaration. - For Functions: Declaring a function
externis usually redundant, as functions have external linkage by default unless declaredstatic. It’s sometimes used for clarity. - Linkage Specification:
extern "C"is used to declare functions that should use C linkage conventions, allowing C++ code to interface with C libraries.
Example with two files sharing a global variable:
File 1: main.cpp
#include <iostream>
// Declaration of 'shared_counter' defined elsewhereextern int shared_counter;
// Declaration of function defined elsewhereextern void print_counter();
int main() { shared_counter = 5; // Modify the shared variable std::cout << "main.cpp: Set shared_counter to " << shared_counter << std::endl; print_counter(); // Call function from another file return 0;}File 2: support.cpp
#include <iostream>
// Definition of the global variable (external linkage by default)int shared_counter = 0; // Initialization happens here
// Definition of the functionvoid print_counter() { std::cout << "support.cpp: shared_counter is " << shared_counter << std::endl;}Compile and link these files (example using g++):
g++ main.cpp support.cpp -o shared_var_example -std=c++11Running ./shared_var_example produces:
main.cpp: Set shared_counter to 5support.cpp: shared_counter is 5The mutable Specifier
Section titled “The mutable Specifier”The mutable specifier applies only to non-static, non-const data members of a class. It allows a member variable to be modified even if it’s part of an object declared const or accessed through a const member function. This is typically used for internal state that doesn’t affect the object’s observable state, like caching or mutexes.
class Example { mutable int cache_value; // Can be modified by const methods int regular_value; bool cache_valid = false;public: Example(int val) : regular_value(val) {}
int getValue() const { // This is a const member function if (!cache_valid) { // Simulate expensive calculation cache_value = regular_value * 2; // OK: cache_value is mutable cache_valid = true; // Error if cache_valid wasn't mutable! // (But making it mutable is often not the right design) } // regular_value = 10; // Error: Cannot modify non-mutable member in const func return cache_value; }};The thread_local Specifier (C++11)
Section titled “The thread_local Specifier (C++11)”The thread_local specifier declares that a variable has thread storage duration. This means each thread in the program will have its own distinct instance of the variable. It’s useful for thread-specific data, like per-thread error codes or random number generators.
#include <iostream>#include <thread>#include <vector>
// Each thread gets its own copy of thread_idthread_local int thread_specific_id = 0;int global_id_counter = 0; // Shared counter
void worker_thread(int id) { thread_specific_id = id; // Set the ID for this thread global_id_counter++; std::cout << "Thread " << std::this_thread::get_id() << ": thread_specific_id = " << thread_specific_id << ", global_id_counter = " << global_id_counter << std::endl;}
int main() { std::vector<std::thread> threads; for (int i = 1; i <= 3; ++i) { threads.emplace_back(worker_thread, i * 100); }
for (auto& t : threads) { t.join(); }
std::cout << "Main thread: thread_specific_id = " << thread_specific_id << ", global_id_counter = " << global_id_counter << std::endl; return 0;}Output might show different thread_specific_id values but an incrementing global_id_counter, demonstrating each thread has its own thread_specific_id.