The C++ Textbook

Part 5 · Generic programming

Deduction and forwarding

How types are worked out, and how to pass them on unchanged.

By the end of this chapter you can

  • Explain the difference between auto and template deduction
  • Write a perfectly forwarding wrapper
  • Explain what a forwarding reference is and how to spot one

Chapter 5.1 said the compiler works out T from the arguments. This chapter is about the rules it uses — because they are not quite what you would guess, and the surprises account for a good share of template bugs.

Deduction drops references and const

What by-value deduction throws away
#include <iostream>
#include <string>
#include <type_traits>

template <class T>
void show_deduced(T) {
    std::cout << "  is reference: " << std::boolalpha << std::is_reference_v<T>
              << ", is const: " << std::is_const_v<std::remove_reference_t<T>> << '\n';
}

int main() {
    std::string value = "hello";
    const std::string& ref = value;

    std::cout << "passing a std::string:\n";        show_deduced(value);
    std::cout << "passing a const std::string&:\n"; show_deduced(ref);
}

Both deduce T = std::string. A by-value parameter always gets its own copy, so the reference and the const are irrelevant to it and are stripped.

auto follows the same rules, which is the connection worth remembering:

auto is template deduction
#include <iostream>
#include <string>
#include <type_traits>

int main() {
    const std::string original = "hello";

    auto copy = original;              // std::string — const dropped
    const auto& observer = original;   // const std::string& — as written
    auto& mutable_ref = const_cast<std::string&>(original);

    std::cout << std::boolalpha;
    std::cout << "copy is const:        " << std::is_const_v<decltype(copy)> << '\n';
    std::cout << "observer is const&:   "
              << std::is_reference_v<decltype(observer)> << '\n';

    copy += " world";                  // legal: it is a separate object
    std::cout << "copy: " << copy << ", original: " << original << '\n';
    (void)mutable_ref;
}

This is the Chapter 1.2 pitfall stated precisely: auto x = container.front(); copies even when front() returns a reference, because plain auto deduces by value and drops the reference. Write auto& or const auto& when you meant to bind.

decltype, and decltype(auto)

decltype(expr) gives the declared type of an expression, keeping references and const:

decltype preserves what auto drops
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

int main() {
    std::vector<std::string> words{"alpha"};

    auto a = words.front();              // std::string — a copy
    decltype(words.front()) b = words.front();   // std::string& — a reference

    std::cout << std::boolalpha;
    std::cout << "auto:     reference? " << std::is_reference_v<decltype(a)> << '\n';
    std::cout << "decltype: reference? " << std::is_reference_v<decltype(b)> << '\n';

    b += "!";
    std::cout << "modifying b changed the container: " << words.front() << '\n';
}

decltype(auto) combines them: deduce like decltype — keeping references — rather than like auto. That matters most for a function that forwards a return value:

Returning what the callee returned
#include <iostream>
#include <type_traits>
#include <vector>

std::vector<int> data{1, 2, 3};

int& element() { return data[0]; }

// auto strips the reference: the caller gets a copy.
auto by_auto() { return element(); }

// decltype(auto) keeps it: the caller gets the reference.
decltype(auto) by_decltype_auto() { return element(); }

int main() {
    std::cout << std::boolalpha;
    std::cout << "by_auto returns a reference:          "
              << std::is_reference_v<decltype(by_auto())> << '\n';
    std::cout << "by_decltype_auto returns a reference: "
              << std::is_reference_v<decltype(by_decltype_auto())> << '\n';

    by_decltype_auto() = 99;
    std::cout << "assigning through it changed data[0]: " << data[0] << '\n';
}

Forwarding references

Here is the rule that looks like a special case and is the whole basis of generic wrappers. In a deduced context, T&& is not an rvalue reference — it is a forwarding reference, and it binds to anything:

One parameter, both value categories
#include <iostream>
#include <string>
#include <type_traits>

template <class T>
void inspect(T&& value) {
    std::cout << "  T is "
              << (std::is_lvalue_reference_v<T> ? "an lvalue reference" : "not a reference")
              << ", parameter binds to "
              << (std::is_lvalue_reference_v<T> ? "an lvalue" : "an rvalue") << '\n';
    (void)value;
}

int main() {
    std::string named = "hello";
    const std::string constant = "world";

    std::cout << "passing an lvalue:\n";        inspect(named);
    std::cout << "passing a const lvalue:\n";   inspect(constant);
    std::cout << "passing an rvalue:\n";        inspect(std::string{"temp"});
}

The mechanism is reference collapsing. When T is deduced as std::string& (for an lvalue), the parameter T&& becomes std::string& &&, which collapses to std::string&. When T is deduced as std::string (for an rvalue), it stays std::string&&. One declaration, both categories.

std::forward

A forwarding reference preserves the category on the way in. Passing it on loses that, because a named parameter is an lvalue — however it was initialised:

Why a named rvalue reference is an lvalue
#include <iostream>
#include <string>
#include <utility>

void consume(const std::string&) { std::cout << "  copy overload\n"; }
void consume(std::string&&)      { std::cout << "  move overload\n"; }

template <class T>
void naive(T&& value) {
    consume(value);                     // `value` is a name: always an lvalue
}

template <class T>
void forwarding(T&& value) {
    consume(std::forward<T>(value));    // restores the original category
}

int main() {
    std::string named = "x";

    std::cout << "naive, lvalue:      "; naive(named);
    std::cout << "naive, rvalue:      "; naive(std::string{"t"});
    std::cout << "forwarding, lvalue: "; forwarding(named);
    std::cout << "forwarding, rvalue: "; forwarding(std::string{"t"});
}

naive calls the copy overload every time, even when handed a temporary — the move is silently lost. std::forward<T> casts back to the original category: an lvalue reference stays an lvalue, an rvalue becomes an rvalue again.

The rule: std::forward<T>(x) on a forwarding reference, std::move(x) on a concrete rvalue reference. std::forward without the template argument does not compile, which is a useful guardrail.

A perfectly forwarding wrapper

Put it together and you get the shape every factory and every emplace uses:

Forwarding a whole argument pack
#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Widget {
    std::string name;
    int size;

    Widget(std::string n, int s) : name(std::move(n)), size(s) {
        std::cout << "  constructed " << name << " (" << size << ")\n";
    }
};

// Forwards any number of arguments, preserving each one's value category.
template <class T, class... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

int main() {
    std::string name = "alpha";

    auto a = make<Widget>(name, 1);                    // name copied
    auto b = make<Widget>(std::string{"beta"}, 2);     // temporary moved
    auto c = make<Widget>(std::move(name), 3);         // name moved

    std::cout << "name after being moved from: \"" << name << "\"\n";
    std::cout << a->name << ' ' << b->name << ' ' << c->name << '\n';
}

Args&&... is a pack of forwarding references and std::forward<Args>(args)... expands to one std::forward per argument. Chapter 5.6 covers packs; this is std::make_unique, essentially in full.

Constraining a forwarding reference

A forwarding reference binds to everything, which makes it greedy — it will beat your copy constructor for a non-const lvalue. Constrain it:

Keeping a greedy template out of the way
#include <concepts>
#include <iostream>
#include <string>
#include <utility>

class Name {
public:
    // Without the constraint, this beats the copy constructor for a
    // non-const Name lvalue, because that needs no qualification conversion.
    template <class T>
        requires (!std::same_as<std::remove_cvref_t<T>, Name>)
    explicit Name(T&& value) : text_(std::forward<T>(value)) {
        std::cout << "  template constructor\n";
    }

    Name(const Name& other) : text_(other.text_) { std::cout << "  copy constructor\n"; }

    const std::string& text() const { return text_; }

private:
    std::string text_;
};

int main() {
    Name a{std::string{"alpha"}};
    Name b{a};                            // must use the copy constructor
    std::cout << b.text() << '\n';
}

std::remove_cvref_t<T> strips references and const so the check sees the underlying type. Without the constraint, Name b{a} calls the template with T = Name&, tries to initialise a std::string from a Name, and fails with an error deep inside the constructor.

Check yourself

Practice