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

638 Views Asked by At

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?
2

There are 2 best solutions below

11
On BEST ANSWER

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.

1
On

is there even a way to disallow a specific type using C++ concepts? (perhaps similar to what I did)

Yes, you can achieve this by adding a constraint that T is not same as the type you don't want.

But remember to remove const, volatile and reference from T if you also don't want them. Because when the function is potentially invoked with a lvalue of type TArg&, T is deduced as TArg& in order to get the argument type as T&& = TArg& && = TArg&. This is reference collapsing and is basically how so-called universal/forwarding-reference works.

template<typename T>
    requires (!std::same_as<Foo, std::remove_cvref_t<T>>)
Foo(T&& t);

//...

Foo f{5};
auto f_clone(f); // calls Foo(const Foo&) with added constraint
                 // (calls Foo(T&&) with T=Foo& without added constraint)

Does applying a template constraint to a type automatically stop the type from being a universal reference?

No, or depends. Constraints are checked after template argument deduction, so forwarding reference still works unless you rule it out with specific constraint like above example code.