Search code examples
c++c++20c++-conceptsforwarding-referenceeffective-c++

Is overloading on universal references now much safer with concepts in c++ 20


In the book "Effective Modern C++" by Scott Meyers the advice is given (item 26/27) to "Avoid overloading on universal references". His rationale for this is that in almost all calls to an overloaded function that includes a universal reference, the compiler resolves to the universal reference even though that is often not the function you intend for it to resolve. (so this code is bad I think?)

template <typename T>
void foo(T&& t) {
  // some sort of perfect forwarding
}
void foo(string&& t) {
  // some sort of move operation
}

The example above is highly contrived and could likely be replaced with 2 functions.

Another example that I think would be harder to resolve and is far less contrived is one he actually gives in Item 26.

class Foo {
// a decent amount of private data that would take a while to copy
public:
  // perfect forwarding constructor, usually the compiler resolves to this...
  template <typename T>
  explicit Foo(T&& t) : /* forward the information for construction */ {}
  // constructor with some sort of rValue
  explicit Foo(int);
// both the below are created by the compiler he says
  // move constructor
  Foo(Foo&& foo);
  // copy constructor (used whenever the value passed in is const)
  Foo(const Foo& foo);
}
// somewhere else in the code
Foo f{5};
auto f_clone(f);

Scott explains that instead of calling a move constructor or copy constructor, the forwarding constructor gets called in auto f_clone(f) because the compiler rules are to resolve to the forward constructor first.

In the book, he explains alternatives to this and a few other examples of overloading on a universal reference. Most of them seem like good solutions for C++11/14/17 but I was thinking there were simpler ways to solve these problems with C++20 concepts. the code would be identical to the code above except for some sort of constraint on the forwarding constructor:

template <typename T>
  requires = !(typename Foo) // not sure what would need to be put here, this is just a guess
explicit Foo(T&& t) : /* forward the information for construction */ {}

I don't know if that would be the correct syntax, I'm super new to C++ concepts

To me, C++ concepts applied to the forwarding function seem like a general solution that could be applied in every case but I'm not sure

there are multiple parts to my question:

  • is there even a way to disallow a specific type using C++ concepts? (perhaps similar to what I did)
  • is there a better way to tell the compiler not to use the forwarding constructor? (I don't want to have to make the variable I'm copying constant or explicitly define the copy/move constructors if I don't need to)
  • If there is a way to do what I'm suggesting, would this be a universally applicable solution to the problem Scott Meyers expresses?
  • Does applying a template constraint to a type automatically stop the type from being a universal reference?

Solution

  • I would say no. I mean, concepts help, because the syntax is nicer than what we had before, but it's still the same problem.

    Here's a real-life example: std::any is constructible from any type that is copy constructible. So there you might start with:

    struct any {
        template <class T>
            requires std::copy_constructible<std::decay_t<T>>
        any(T&&);
    
        any(any const&);
    };
    

    The problem is, when you do something like this:

    any a = 42; // calls any(T&&), with T=int
    any b = a;  // calls any(T&&), with T=any
    

    Because any itself is, of course, copy constructible. This makes the constructor template viable, and a better match since it's a less-const-qualified reference.

    So in order to avoid that (because we want b to hold an int, and not hold an any that holds an int) we have to remove ourselves from consideration:

    struct any {
        template <class T>
            requires std::copy_constructible<std::decay_t<T>>
                  && (!std::same_as<std::decay_t<T>, any>)
        any(T&&);
    
        any(any const&);
    };
    

    This is the same thing we had to do in C++17 and earlier when Scott Meyers wrote his book. At least, it's the same mechanism for resolving the problem - even if the syntax is better.