Search code examples
c++c++11compiler-errorsmove-semanticsoverload-resolution

Why constructor Message(const T& data) is conflict with Message(T&& data) when T = int&?


template <typename T>
struct Message {
    T data;
    explicit Message(T&& data) : data(std::move(data)) {
        std::cout << "Move data" << std::endl;
    }

    explicit Message(const T& data) : data(data) {
        std::cout << "Copy data" << std::endl;
    }
};

template <typename T>
inline Message<T>*
makeMessage(T&& data) {
    return new Message<T>{std::forward<T>(data)};
}


int main() {
    const int a = 1024;
    auto *copy_msg = makeMessage(a);
}

There is a template class Message that has two constructors: Message(T&& data) and Message(const T& data), And I got the following compile-time errors when I called makeMessage(a).

error: multiple overloads of 'Message' instantiate to the same signature 'void (const int &&)'

explicit Message(const T& data) : data(data) {

previous declaration is here

explicit Message(T&& data) : data(std::move(data)) {

However, It works when I called make_message(1024) and make_message(std::move(a)).

So why constructor Message(const T& data) is duplicate with Message(T&& data) when T = int&?


Solution

  • So why constructor Message(const T& data) is duplicate with Message(T&& data) when T = int&?

    Because of reference collapsing rules.

    There is no such thing as a reference to reference. When T is an lvalue reference - let's say int &, then syntactically T & appears to be a int & &. But such type does not exist. The rules of the language say that in such case T & collapses into int &.

    Similarly, there is no such thing as const reference (not to be confused with reference to const, which people sometimes mean when they say const reference). When T is an lvalue reference - let's say int&, then syntactically T const (same as const T) appears to be a int& const (not to be confused with int const &, which is same as const int &). But such type does not exist. The rules of the language say that in such case T const collapses into int &.

    C++11 introduced rvalue references, which brings us new collapsing rules. In short, "rvalue reference to rvalue reference" collpses into rvalue reference, but "lvalue reference to any reference", as well as "any reference to lvalue reference" collapse into lvalue reference. This rule is part of the magic behind how forwarding references work.

    As such, given T = int &, these declare the same function:

    explicit Message(T&& data)      // type of argument is int &
    explicit Message(const T& data) // type of argument is int &
    

    Bonus: There are also collapsing rules for non-references: There is no such thing as const const type. As such, given const T where T is const int, then it collapses into const int. Same goes for volatile.