It is often recommended to always pass function and constructor parameters by const&.

For “in” parameters, pass cheaply-copied types by value and others by reference to const.

If the function is going to keep a copy of the argument, in addition to passing by const&, add an overload that passes the parameter by && and in the body std::move it to its destination.

C++ Core Guidelines

The issue with this recommendation is when the function/constructor takes multiple arguments who all need to be copied.

struct Copier {
    Copier(const Debug& a, const Debug& b) : a(a), b(b) {}
    Debug a, b;
};

What if we wanted to move the objects when calling the constructor, rather than copying every time? Following the usual recommendation, we could create a && overload. This works if, when calling this constructor, we move both arguments, as in Copier(std::move(a), std::move(b)).

Copier(Debug&& a, Debug&& b) : a(std::move(a)), b(std::move(b)) {}

However, this will NOT work if we can only move one argument. If we call Copier(std::move(a), b)), then the const& constructor overload will be selected, and BOTH values will be copied. We could write two more overloads, but as you can imagine this quickly makes the API pretty messy as it explodes the necessary constructor overloads.

The solution here is to only have one constructor, taking all parameters than need to be copied by value. This will let the caller copy or move each argument separately.

Copier(Debug a, Debug b) : a(std::move(a)), b(std::move(b)) {}

Rule of Thumb

My personal rule of thumb: if the function/constructor will need to keep a copy of the object anyway, then pass by value. This lets the caller make the copy or move the value. It also keeps the API clean with only one function/constructor rather than many const& and && overloads. The only drawback is that it causes an extra move operation, which is almost always negligible.

Herb Sutter also discussed this issue in his CppCon 2014 talk, “Back to Basics! Modern C++ Style” (1:25:50).

Compiler Explorer

#include <iostream>

struct Debug {
    Debug() = default;

    Debug(const Debug&) {
        std::cout << "Copied!" << std::endl;
    }

    Debug(Debug&&) {
        std::cout << "Moved!" << std::endl;
    }
};

struct Copier {
    Copier(const Debug& a, const Debug& b) : a(a), b(b) {}
    Copier(Debug&& a, Debug&& b) : a(std::move(a)), b(std::move(b)) {}
    Debug a, b;
};

struct ByValueCopier {
    ByValueCopier(Debug a, Debug b) : a(std::move(a)), b(std::move(b)) {}
    Debug a, b;
};

int main() {
    Debug a, b;

    std::cout << "1" << std::endl;
    Copier(a, b);
    std::cout << "2" << std::endl;
    Copier(std::move(a), b);
    std::cout << "3" << std::endl;
    Copier(std::move(a), std::move(b));
    
    std::cout << std::endl;

    std::cout << "4" << std::endl;
    ByValueCopier(a, b);
    std::cout << "5" << std::endl;
    ByValueCopier(std::move(a), b);
    std::cout << "6" << std::endl;
    ByValueCopier(std::move(a), std::move(b));
}