Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. In-class initializers are applied first
  2. Constructor initializer list overrides in-class initializers
  3. 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.