In-Class Member Initialization (C++11)
Introduction
In-class member initialization (also called default member initialization) was introduced in C++11 and represents a significant improvement in how we initialize class data members. This feature allows you to specify default values for non-static data members directly in the class definition, rather than solely in constructors.
The Problem Before C++11
Before C++11, the only way to initialize non-static data members was through constructor initializer lists or in the constructor body. This created several issues:
Issue 1: Repetitive Initialization Code
// Pre-C++11: Repetitive and error-prone
class Server {
private:
std::string host;
int port;
int timeout;
bool ssl_enabled;
int max_connections;
public:
// Default constructor - must initialize everything
Server()
: host("localhost"),
port(8080),
timeout(30),
ssl_enabled(false),
max_connections(100) {
}
// Parameterized constructor - must repeat defaults
Server(const std::string& h, int p)
: host(h),
port(p),
timeout(30), // Repeated!
ssl_enabled(false), // Repeated!
max_connections(100) { // Repeated!
}
// Another constructor - more repetition
Server(const std::string& h, int p, bool ssl)
: host(h),
port(p),
timeout(30), // Repeated again!
ssl_enabled(ssl),
max_connections(100) { // Repeated again!
}
};
Problems:
- Default values duplicated across multiple constructors
- Easy to forget a member in one constructor
- Maintenance nightmare when changing default values
- Inconsistency risk across constructors
Issue 2: Mandatory Default Constructor
// Pre-C++11: Forced to write default constructor just for initialization
class Configuration {
private:
int retry_count;
double timeout_seconds;
bool auto_reconnect;
public:
// Must write this just to set defaults
Configuration()
: retry_count(3),
timeout_seconds(5.0),
auto_reconnect(true) {
}
};
Issue 3: Const and Reference Members Were Painful
// Pre-C++11: Const members required initialization in ALL constructors
class Document {
private:
const std::string document_id; // Must initialize in every constructor
const int version; // Must initialize in every constructor
std::string content;
public:
// Every constructor must initialize const members
Document(const std::string& id)
: document_id(id),
version(1), // Always the same
content("") {
}
Document(const std::string& id, const std::string& text)
: document_id(id),
version(1), // Duplicated!
content(text) {
}
Document(const std::string& id, int ver, const std::string& text)
: document_id(id),
version(ver),
content(text) {
}
};
In-Class Member Initialization (C++11)
C++11 introduced the ability to initialize non-static data members directly at their point of declaration.
Basic Syntax
class MyClass {
private:
int value = 42; // Direct initialization
std::string name = "default"; // Works with any type
double pi{3.14159}; // Brace initialization also works
bool flag = false;
};
Solving the Repetition Problem
// C++11: Clean and DRY (Don't Repeat Yourself)
class Server {
private:
std::string host = "localhost";
int port = 8080;
int timeout = 30;
bool ssl_enabled = false;
int max_connections = 100;
public:
// Default constructor becomes trivial (or can be omitted)
Server() = default;
// Only specify what changes from defaults
Server(const std::string& h, int p)
: host(h), port(p) {
// timeout, ssl_enabled, max_connections use in-class defaults
}
// Selectively override defaults
Server(const std::string& h, int p, bool ssl)
: host(h), port(p), ssl_enabled(ssl) {
// Other members use in-class defaults
}
};
No More Mandatory Default Constructor
// C++11: Default constructor often not needed
class Configuration {
private:
int retry_count = 3;
double timeout_seconds = 5.0;
bool auto_reconnect = true;
public:
// No need to define default constructor - compiler generates one
// that uses in-class initializers
// Can still add parameterized constructors
explicit Configuration(int retries)
: retry_count(retries) {
}
};
// Usage
Configuration cfg1; // Uses all defaults
Configuration cfg2{10}; // Uses custom retry_count
Const Non-Static Data Members
In-class member initialization significantly simplifies working with const non-static data members.
Before C++11: Const Members Were Painful
// Pre-C++11: Must initialize const members in EVERY constructor
class Product {
private:
const std::string product_id; // Const - can't be changed after construction
const double tax_rate; // Const - fixed value
std::string name;
double price;
public:
// Constructor 1 - must initialize all const members
Product(const std::string& id, const std::string& n, double p)
: product_id(id),
tax_rate(0.08), // Always 0.08, but must repeat
name(n),
price(p) {
}
// Constructor 2 - must initialize all const members again
Product(const std::string& id, const std::string& n, double p, double tax)
: product_id(id),
tax_rate(tax),
name(n),
price(p) {
}
// Can't have default constructor without default product_id
// Product() { } // ERROR! Const members not initialized
};
C++11: Const Members with In-Class Initialization
// C++11: Much cleaner with in-class initialization
class Product {
private:
const std::string product_id; // Must still be initialized in constructor
const double tax_rate = 0.08; // Can have default value!
std::string name = "Unnamed"; // Non-const can also have default
double price = 0.0;
public:
// Constructor only needs to initialize what doesn't have defaults
Product(const std::string& id, const std::string& n, double p)
: product_id(id), // Must initialize (no default possible)
name(n),
price(p) {
// tax_rate uses in-class default (0.08)
}
// Can override the const default if needed
Product(const std::string& id, const std::string& n, double p, double tax)
: product_id(id),
tax_rate(tax), // Overrides default
name(n),
price(p) {
}
};
Important Rules for Const Members
class Example {
private:
// ✅ Const members CAN have in-class initializers
const int fixed_value = 100;
const std::string constant_name = "Example";
// ✅ Const members without defaults must be initialized in constructor
const int must_initialize_in_constructor;
// ✅ Can override in-class initializer in constructor
const int can_override = 50;
public:
Example(int value)
: must_initialize_in_constructor(value),
can_override(value * 2) { // Overrides the default 50
// fixed_value and constant_name use in-class defaults
}
// ❌ Cannot modify const members after construction
void setValue(int v) {
// fixed_value = v; // ERROR! Cannot modify const
}
};
Const Members: Common Patterns
// Pattern 1: Configuration with const settings
class DatabaseConnection {
private:
const std::string connection_string; // Must be set in constructor
const int max_pool_size = 10; // Has reasonable default
const int timeout_seconds = 30; // Has reasonable default
bool is_connected = false; // Non-const, can change
public:
explicit DatabaseConnection(const std::string& conn_str)
: connection_string(conn_str) {
// max_pool_size and timeout_seconds use defaults
}
DatabaseConnection(const std::string& conn_str, int pool_size)
: connection_string(conn_str),
max_pool_size(pool_size) {
// timeout_seconds uses default
}
};
// Pattern 2: Immutable identifier with defaults
class Transaction {
private:
const std::string transaction_id;
const std::chrono::system_clock::time_point timestamp =
std::chrono::system_clock::now();
const std::string currency = "USD"; // Default currency
public:
explicit Transaction(const std::string& id)
: transaction_id(id) {
// timestamp and currency use defaults
}
Transaction(const std::string& id, const std::string& curr)
: transaction_id(id),
currency(curr) {
// timestamp uses default
}
};
How It Reduces Constructor Headaches
Benefit 1: Fewer Constructors Needed
// Before C++11: Need multiple constructors for different defaults
class Window {
private:
int width;
int height;
bool visible;
bool resizable;
std::string title;
public:
Window()
: width(800), height(600), visible(true),
resizable(true), title("Window") {}
Window(int w, int h)
: width(w), height(h), visible(true),
resizable(true), title("Window") {}
Window(int w, int h, const std::string& t)
: width(w), height(h), visible(true),
resizable(true), title(t) {}
// ... more constructors for different combinations
};
// C++11: One or two constructors handle everything
class Window {
private:
int width = 800;
int height = 600;
bool visible = true;
bool resizable = true;
std::string title = "Window";
public:
// Default constructor - not even needed, compiler generates it
Window() = default;
// One flexible constructor covers most cases
Window(int w, int h, const std::string& t = "Window")
: width(w), height(h), title(t) {
// visible and resizable use defaults
}
};
Benefit 2: Constructor Delegation Made Simpler
// C++11: Delegating constructors + in-class initialization
class User {
private:
std::string username;
std::string email;
bool is_admin = false; // Default for most users
int login_attempts = 0; // Fresh start
bool account_locked = false; // Not locked initially
public:
// Primary constructor
User(const std::string& name, const std::string& mail)
: username(name), email(mail) {
// is_admin, login_attempts, account_locked use defaults
}
// Delegating constructor for admin
User(const std::string& name, const std::string& mail, bool admin)
: User(name, mail) { // Delegate to primary constructor
is_admin = admin; // Only override what's different
}
};
Benefit 3: Consistent Defaults Across Inheritance
class Base {
protected:
int base_value = 100; // Default in base class
bool base_flag = true;
public:
Base() = default;
explicit Base(int val) : base_value(val) {}
};
class Derived : public Base {
private:
int derived_value = 200; // Derived's own default
std::string name = "Derived";
public:
// Default constructor uses all in-class defaults
Derived() = default;
// Can initialize base and derived selectively
Derived(int base_val, int derived_val)
: Base(base_val),
derived_value(derived_val) {
// name uses default, base_flag uses default
}
};
Benefit 4: Less Error-Prone Maintenance
// Scenario: Need to change default timeout from 30 to 60 seconds
// Before C++11: Update in multiple places (error-prone)
class Service {
private:
int timeout;
public:
Service() : timeout(30) {} // Change here
Service(const std::string& url) : timeout(30) {} // And here
Service(const std::string& url, int retries) : timeout(30) {} // And here
// Easy to miss one!
};
// C++11: Change in ONE place only
class Service {
private:
int timeout = 30; // Change ONLY here
public:
Service() = default;
Service(const std::string& url) { }
Service(const std::string& url, int retries) { }
// All constructors automatically use updated default
};
Benefit 5: Cleaner Move and Copy Constructors
class Resource {
private:
std::unique_ptr<int> data;
int ref_count = 0; // Always start at 0
bool is_valid = true; // Always start valid
public:
Resource() : data(std::make_unique<int>(42)) {}
// Move constructor - only handle complex members
Resource(Resource&& other) noexcept
: data(std::move(other.data)) {
// ref_count and is_valid automatically initialized to defaults
other.is_valid = false;
}
// Copy constructor
Resource(const Resource& other)
: data(std::make_unique<int>(*other.data)) {
// ref_count and is_valid automatically use in-class defaults
}
};
Initialization Order and Priority
Understanding the initialization order is crucial:
Priority Rules
- In-class initializers are applied first
- Constructor initializer list overrides in-class initializers
- Constructor body can modify (but not initialize const members)
class Example {
private:
int a = 10; // In-class initializer
int b = 20;
int c = 30;
public:
Example() {
// a = 10, b = 20, c = 30 (all use in-class defaults)
}
Example(int x)
: a(x) { // Constructor initializer list overrides
// a = x, b = 20, c = 30
}
Example(int x, int y)
: a(x), b(y) { // Multiple overrides
c = 40; // Can still modify in body (if not const)
// a = x, b = y, c = 40
}
};
What Happens Behind the Scenes
class Demo {
private:
int value = 100;
std::string name = "Default";
public:
Demo(int v) : value(v) {
std::cout << "Constructor body\n";
}
};
// Conceptually equivalent to:
class Demo {
private:
int value;
std::string name;
public:
Demo(int v)
: value(v), // Constructor list takes priority
name("Default") { // In-class initializer applied
std::cout << "Constructor body\n";
}
};
Syntax Options
C++11 supports multiple initialization syntaxes for in-class member initialization:
class SyntaxExamples {
private:
// ✅ Copy initialization (most common)
int a = 42;
std::string name = "example";
// ✅ Brace initialization (preferred for preventing narrowing)
int b{42};
double pi{3.14159};
std::vector<int> vec{1, 2, 3};
// ❌ Parentheses NOT allowed for in-class initialization
// int c(42); // ERROR in C++11/14/17
// std::string s("hi"); // ERROR in C++11/14/17
// Note: C++20 allows parentheses in some cases
};
Static vs Non-Static Members
Important distinction between static and non-static initialization:
class MemberTypes {
private:
// ✅ Non-static: Can use in-class initialization (C++11)
int non_static = 42;
std::string name = "example";
// ✅ Static const integral: Could always be initialized in-class
static const int static_const = 100;
// ✅ Static constexpr: Can be initialized in-class (C++11)
static constexpr double pi = 3.14159;
// ❌ Static non-const: Still needs out-of-class definition (until C++17)
static int static_value; // Declared here
// ✅ C++17: inline static can be initialized in-class
inline static int inline_static = 200;
public:
void print() {
std::cout << non_static << ", " << static_const << "\n";
}
};
// Out-of-class definition still needed for static non-const (pre-C++17)
int MemberTypes::static_value = 50;
Best Practices
1. Use In-Class Initialization for Defaults
// ✅ Good: Clear default values
class Config {
private:
int timeout = 30;
bool debug_mode = false;
std::string log_file = "app.log";
};
// ❌ Avoid: Initializing in constructor when default makes sense
class Config {
private:
int timeout;
bool debug_mode;
public:
Config() : timeout(30), debug_mode(false) {} // Unnecessary
};
2. Prefer Brace Initialization for Safety
class SafeInit {
private:
int value{42}; // ✅ Prevents narrowing
double pi{3.14159}; // ✅ Consistent with uniform initialization
// int x{3.14}; // ❌ Error: narrowing conversion
};
3. Document Non-Default Values
class Service {
private:
int retry_count = 3; // Standard retry count
int timeout_ms = 5000; // 5 second timeout
bool use_compression = true; // Enable compression by default
// Special value - document why it's different
int buffer_size = 8192; // Must match OS page size
};
4. Use with Constructor Delegation
class User {
private:
std::string name;
int age = 0;
bool active = true;
public:
// Primary constructor
User(const std::string& n) : name(n) {}
// Delegate and override specific members
User(const std::string& n, int a) : User(n) {
age = a;
}
};
Common Pitfalls and Solutions
Pitfall 1: Order of Initialization
class BadOrder {
private:
int a = b + 1; // ❌ Problem: b not initialized yet!
int b = 10; // Member declaration order matters
public:
BadOrder() {
// a is initialized with undefined b value
}
};
// ✅ Solution: Be aware of declaration order
class GoodOrder {
private:
int b = 10; // Declare first
int a = b + 1; // Then use it
};
Pitfall 2: Expensive Initialization
class Expensive {
private:
std::vector<int> data = createLargeVector(); // ❌ Called for every object
static std::vector<int> createLargeVector() {
return std::vector<int>(1000000, 0);
}
};
// ✅ Solution: Use default constructor or lazy initialization
class Better {
private:
std::vector<int> data; // Start empty
public:
void ensureData() {
if (data.empty()) {
data = createLargeVector();
}
}
};
Summary
In-class member initialization, introduced in C++11, dramatically simplified C++ class initialization by:
Key Benefits:
- Eliminates repetition across multiple constructors
- Reduces bugs from inconsistent defaults
- Simplifies maintenance - change defaults in one place
- Reduces constructor count - often don’t need default constructor
- Works with const members - provide reasonable defaults
- Clearer intent - defaults visible at member declaration
- Better for generated code - compiler can optimize better
Best For:
- Default values that apply to most cases
- Configuration classes with many optional parameters
- Const members with standard defaults
- Classes with multiple constructors
- Simple, consistent initialization values
Remember:
- Constructor initializer list overrides in-class initializers
- Member declaration order matters for initialization
- Works with brace and copy initialization syntax
- C++20 added parentheses syntax support
- Combines perfectly with delegating constructors
In-class member initialization represents a significant quality-of-life improvement in modern C++, making code cleaner, safer, and more maintainable.