Skip to content

Latest commit

 

History

History
252 lines (187 loc) · 12.2 KB

stdref.md

File metadata and controls

252 lines (187 loc) · 12.2 KB

std::ref, std::cref, and std::reference_wrapper


Executive Abstract

std::reference_wrapper<T> is a class template that wraps a reference to an object or function of type $T$ into a copyable, assignable, first-class object representation. std::ref and std::cref are helper factory functions that generate std::reference_wrapper<T> and std::reference_wrapper<const T> instances. Introduced in C++11 and enhanced with constexpr capabilities in C++20, these utilities overcome fundamental language limitations of native C++ references—enabling non-owning reference semantics inside STL containers (std::vector<std::reference_wrapper<T>>) and overriding forced value-decay in parameter-decaying templates (std::thread, std::bind, std::async).


Motivation & Historical Context

Pre-C++11 Deficiencies: The Limitations of Native References

Native C++ references (T&) are fundamental language aliases, but possess two strict design constraints:

  1. Non-Rebindable (Non-Assignable): A native reference must be initialized upon declaration and cannot be rebound to refer to another object. Writing ref1 = obj2; mutates the value of the object currently bound to ref1, rather than updating ref1 to point to obj2.
  2. Incompatible with Standard Library Containers: STL containers (std::vector, std::list) require stored elements to be Erasable and CopyAssignable. Declaring std::vector<int&> is illegal C++ syntax.

The Parameter-Decay Problem in Generic Libraries:

Template functions such as std::thread, std::bind, std::make_pair, and std::async apply std::decay to their template parameters, forcing arguments to be copied by value into internal state structures:

// C++11 Parameter Decay Conundrum
void worker(int& counter) { ++counter; }

int main() {
    int val = 0;

    // std::thread decay-copies 'val' into its thread context parameter buffer!
    // std::thread t(worker, val); // COMPILE ERROR! Cannot pass copied 'int' to 'int&'!
}

Solution: C++11 introduced std::reference_wrapper alongside std::ref and std::cref. Passing std::ref(val) creates a copyable value object holding val's address. Decaying templates copy the wrapper by value, allowing worker functions to dereference the wrapper and mutate the original variable val in caller scope.


Technical Mechanics & Internal Architecture

1. Internal Storage & Re-binding Mechanics

Under the hood, std::reference_wrapper<T> encapsulates a non-null raw pointer T* m_ptr:

// Conceptual ISO Standard Implementation of std::reference_wrapper
template <class T>
class reference_wrapper {
private:
    T* m_ptr; // Internally stores address of target object

public:
    // Constructed from non-const lvalue reference
    constexpr reference_wrapper(T& ref) noexcept : m_ptr(std::addressof(ref)) {}

    // Deletes rvalue constructors to prevent dangling references to temporaries!
    reference_wrapper(T&&) = delete;

    // Copy Constructor & Assignment REBIND m_ptr to a new target object!
    constexpr reference_wrapper(const reference_wrapper&) noexcept = default;
    constexpr reference_wrapper& operator=(const reference_wrapper&) noexcept = default;

    // Implicit conversion operator to native T&
    constexpr operator T& () const noexcept { return *m_ptr; }

    // Explicit accessor
    constexpr T& get() const noexcept { return *m_ptr; }
};

2. Callable Target Invocation (operator())

If target type $T$ is a Callable entity (free function, class functor, lambda), std::reference_wrapper<T> overloads operator()(Args&&... args), executing std::invoke(*m_ptr, std::forward<Args>(args)...). This allows callable reference wrappers to act as lightweight, non-owning polymorphic function references.


3. Overload Deleted Safety Protection

To prevent immediate Dangling Reference Undefined Behavior, std::ref and std::cref explicitly delete rvalue overloads:

std::ref(42);         // COMPILE ERROR! Deleted overload for rvalues!
std::ref(std::string("temp")); // COMPILE ERROR! Cannot bind reference_wrapper to temporary!

Evolution Across Standards (C++11 to C++26/C++29)

┌───────────────────────────────────────────────────────────────────┐
│ C++11                                                             │
├───────────────────────────────────────────────────────────────────┤
│ • Introduced std::reference_wrapper<T>, std::ref, and std::cref   │
│ • Enabled non-owning reference storage in STL containers          │
│ • Solved argument decay in std::thread and std::bind              │
└───────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌───────────────────────────────────────────────────────────────────┐
│ C++14 / C++17                                                     │
├───────────────────────────────────────────────────────────────────┤
│ • Added std::unwrap_reference and std::unwrap_ref_decay traits    │
│ • Class Template Argument Deduction (CTAD) for reference_wrapper  │
└───────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌───────────────────────────────────────────────────────────────────┐
│ C++20                                                             │
├───────────────────────────────────────────────────────────────────┤
│ • Complete constexpr conversion for reference_wrapper and helpers │
│ • Interoperability with C++20 Ranges (std::views::all)            │
└───────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌───────────────────────────────────────────────────────────────────┐
│ C++23 / C++26                                                     │
├───────────────────────────────────────────────────────────────────┤
│ • Refined constexpr evaluation in tuple decomposition & algorithms│
└───────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Overriding Parameter Decay in Multi-Threaded Execution

Demonstrates using std::ref to pass mutable lvalue references into std::thread and std::async.

#include <iostream>
#include <thread>
#include <functional>

void increment_counter(int& counter, int iterations) {
    for (int i = 0; i < iterations; ++i) {
        ++counter;
    }
}

int main() {
    int shared_counter = 0;

    // std::thread decay-copies its arguments.
    // Wrapping 'shared_counter' in std::ref() passes a copyable reference wrapper,
    // allowing 'increment_counter' to mutate 'shared_counter' directly!
    std::thread t(increment_counter, std::ref(shared_counter), 1000);
    t.join();

    std::cout << "Final Counter Value: " << shared_counter << " (Expected: 1000)\n";
    return 0;
}

Example 2: Non-Owning Polymorphic Collections in STL Containers

Demonstrates storing polymorphic references in std::vector<std::reference_wrapper<T>> without dynamic heap memory allocation or smart pointer overhead.

#include <iostream>
#include <vector>
#include <functional>
#include <string>

struct Shape {
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

struct Circle : public Shape {
    void draw() const override { std::cout << "Drawing Circle\n"; }
};

struct Square : public Shape {
    void draw() const override { std::cout << "Drawing Square\n"; }
};

int main() {
    Circle c;
    Square s;

    // std::vector<Shape&> is ILLEGAL syntax in C++!
    // std::vector<std::reference_wrapper<Shape>> provides non-owning polymorphic reference storage:
    std::vector<std::reference_wrapper<const Shape>> canvas;

    canvas.push_back(std::cref(c)); // Store const reference to c
    canvas.push_back(std::cref(s)); // Store const reference to s

    std::cout << "Rendering Canvas Elements:\n";
    for (const Shape& shape : canvas) {
        shape.draw(); // Implicit conversion from reference_wrapper to Shape&!
    }

    return 0;
}

Example 3: C++20 constexpr Callable Reference Wrappers

Demonstrates wrapping callable closures and invoking them inside compile-time constant evaluation.

#include <iostream>
#include <functional>

constexpr int execute_callable(std::reference_wrapper<int(int, int)> func_ref, int a, int b) {
    return func_ref(a, b); // Invokes underlying free function via overloaded operator()
}

constexpr int add_func(int x, int y) { return x + y; }

int main() {
    // C++20: std::reference_wrapper is fully constexpr-capable!
    constexpr auto ref_wrapper = std::ref(add_func);
    constexpr int result = execute_callable(ref_wrapper, 20, 22);

    static_assert(result == 42, "Compile-time reference wrapper call failed");
    std::cout << "Compile-time callable reference result: " << result << "\n";

    return 0;
}

Engineering Best Practices & Common Pitfalls

  1. Beware of Dangling References (Use-After-Free):

    • std::reference_wrapper does not extend the lifetime of its target object. If the target object goes out of scope while the reference_wrapper remains, calling .get() results in Use-After-Free Undefined Behavior.
  2. Use std::vector<std::reference_wrapper<T>> for Non-Owning Collections:

    • Prefer std::vector<std::reference_wrapper<T>> over std::vector<T*> when building non-owning collections of existing objects to maintain clean reference syntax (obj.method()) and eliminate null pointer dereference checks.
  3. Re-binding Reference Wrappers:

    • Assigning one reference_wrapper to another rebinds the pointer internal member (m_ptr). It does not invoke the assignment operator of the underlying target object.

Relationships

  • Related to:
    • std::thread / std::async — Primary APIs requiring std::ref to override parameter decay.
    • std::unwrap_reference — Type trait extracting $T$ from std::reference_wrapper<T>.
    • Lambda Expressions — Closures that can be passed by reference via std::ref.
  • Superseded by: None.
  • Supersedes: Raw pointers (T*) used solely to simulate re-assignable reference wrappers in STL containers.

Back to C++11 section.