Why Do C++11-Deleted Functions Participate in Overload Resolution

Why do C++11-deleted functions participate in overload resolution?

Half of the purpose of the = delete syntax is to be able to prevent people from calling certain functions with certain parameters. This is mainly to prevent implicit conversions in certain specific scenarios. In order to forbid a particular overload, it has to participate in overload resolution.

The answer you cite gives you a perfect example:

struct onlydouble {
onlydouble(std::intmax_t) = delete;
onlydouble(double);
};

If delete removed the function entirely, that would make the = delete syntax equivalent to this:

struct onlydouble2 {
onlydouble2(double);
};

You could do this:

onlydouble2 val(20);

This is legal C++. The compiler will look at all constructors; none of them take an integer type directly. But one of them can take it after an implicit conversion. So it'll call that.

onlydouble val(20);

This is not legal C++. The compiler will look at all constructors, including the deleted ones. It will see an exact match, via std::intmax_t (which will exactly match any integer literal). So the compiler will select it and then immediately issue an error, because it selected a deleted function.

= delete means "I forbid this," not merely, "This does not exist." It's a much stronger statement.

I was asking why the C++ standard says = delete means "I forbid this" instead of "this does not exist"

It's because we don't need special grammar to say "this does not exist." We get this implicitly by simply not declaring the particular "this" in question. "I forbid this" represents a construct that cannot be achieved without special grammar. So we get special grammar to say "I forbid this" and not the other thing.

The only functionality you would gain by having an explicit "this does not exist" grammar would be to prevent someone from later declaring it to exist. And that's just not useful enough to need its own grammar.

there is otherwise no way to declare that the copy constructor does not exist, and its existence can cause nonsensical ambiguities.

The copy constructor is a special member function. Every class always has a copy constructor. Just as they always have a copy assignment operator, move constructor, etc.

These functions exist; the question is only whether it is legal to call them. If you tried to say that = delete meant that they didn't exist, then the specification would have to explain what it means for a function to not exist. This is not a concept that the specification handles.

If you attempt to call a function that hasn't been declared/defined yet, then the compiler will error. But it will error because of an undefined identifier, not because of a "function doesn't exist" error (even if your compiler reports it that way). Various constructors are all called by overload resolution, so their "existence" is handled in that regard.

In every case, there is either a function declared via identifier, or a constructor/destructor (also declared via identifier, just a type-identifier). Operator overloading hides the identifier behind syntactic sugar, but it's still there.

The C++ specification cannot handle the concept of a "function that does not exist." It can handle an overload mismatch. It can handle an overload ambiguity. But it doesn't know about what isn't there. So = delete is defined in terms of the far more useful "attempts to call this fail" rather than the less useful "pretend I never wrote this line."

And again, re-read the first part. You cannot do that with "function doesn't exist." That's another reason why it's defined that way: because one of the main use cases of the = delete syntax is to be able to force the user to use certain parameter types, to explicitly cast, and so forth. Basically, to foil implicit type conversions.

Your suggestion would not do that.

Deleting overloaded function. C++11. Call of overloaded ... is ambiguous

When you call

func(2.3)

you pass a double to the function. The list of candidates contains both func(int) and func(char), as overload resolution happens before delete kicks in:

If the function is overloaded, overload resolution takes place first, and the program is only ill-formed if the deleted function was selected Ref: cppreference.com, see Avishai's answer for precise standard quotes.

Now double can be converted both to char and int, hence the ambiguity.

Without deleting function with char arguments double was converted to int and func(int) was called, why now it is forbidden?

You get an ambiguity error even without deleteing the char version, see live here. Of course if you only define the func(int), then there will be no ambiguity so double will happily be converted to an int.

Why doesn't std::function participate in overload resolution?

This doesn't really have anything to do with "phases of translation". It's purely about the constructors of std::function.

See, std::function<R(Args)> doesn't require that the given function is exactly of the type R(Args). In particular, it doesn't require that it is given a function pointer. It can take any callable type (member function pointer, some object that has an overload of operator()) so long as it is invokable as if it took Args parameters and returns something convertible to R (or if R is void, it can return anything).

To do that, the appropriate constructor of std::function must be a template: template<typename F> function(F f);. That is, it can take any function type (subject to the above restrictions).

The expression baz represents an overload set. If you use that expression to call the overload set, that's fine. If you use that expression as a parameter to a function that takes a specific function pointer, C++ can whittle down the overload set to a single call, thus making it fine.

However, once a function is a template, and you're using template argument deduction to figure out what that parameter is, C++ no longer has the ability to determine what the correct overload in the overload set is. So you must specify it directly.

Why does overload resolution not fall back to constructor with reference to base class when copy-constructor is deleted?

Because = delete is a still a definition of the constructor (it is defined as deleted). Deleted functions are still possible candidates in overload resolution, and, in this case, the copy constructor is still the most viable candidate, and it is selected. Since a deleted function is referred to, the program is ill-formed.

From the standard, [dcl.fct.def.delete]:

  1. A deleted definition of a function is a function definition whose function-body is of the form = delete ; [...] A deleted function is a function with a deleted definition or a function that is implicitly defined as deleted.
  2. A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed.

And it is pretty easy to see why a definition for B::B(B&) is more viable than B::B(Base&) for an lvalue of type B

Function doesn't participate in overload resolution in inheritance

Overloading won't take place through different scopes.

For fun(42.42f);, the name fun has to be found. According to the rule of unqualified name lookup:

For an unqualified name, that is a name that does not appear to the right of a scope resolution operator ::, name lookup examines the scopes as described below, until it finds at least one declaration of any kind, at which time the lookup stops and no further scopes are examined.

That means the name will be found at the scope of class Derived, i.e. Derived::fun(). Then the name lookup stops, the names in the scope of Base won't be considered at all.

If you remove the definition of Derived::fun, there would be no names found at the scope of Derived, then the scope of the base class (i.e. Base) will be examined further and Base::foo will be found. Then everything works fine.

It's also worth noting that overload resolution happens after name lookup.

With std::optional, what does it mean to remove the move constructor from overload resolution?

Yes, SFINAE does not work for constructors, use base classes forcing the compiler to do the right thing.

It means it is not defined and the class cannot be move constructed. More interesting question is why is it needed?

I am not 100% sure I have the right answer to that.

TL;DR Returning std::optional<NonMoveable> generates compiler errors if the move constructor of optional is present. On the other hand, returning NonMoveable directly fallbacks to copy constructor.

First, the constraint does not break anything. The constructor cannot be implemented if T cannot be move constructed.

Second, all methods of std::optional are very tricky due to std::optional<std::optional<T>> issue which could easily lead to ambiguous calls if proper constraints are not taken, optional(U&& value) is really susceptible to this.

The main reason is, I believe, because we want optional<T> to act as T whenever possible and there is one edge case, that I am aware of, when the existence of std::optional's move constructor for non-moveable T leads to unnecessary compiler errors. Coincidentally, it is the case of returning std::optional by value from functions, something I do very often.

For a variable x of type T, return x in a function T foo() calls move constructor if it accessible, copy if not.

Take these simple definitions:

#include <utility>
struct CopyOnly {
CopyOnly() = default;
CopyOnly(const CopyOnly &) = default;
CopyOnly(CopyOnly &&) = delete;

CopyOnly &operator=(const CopyOnly &) = default;
CopyOnly &operator=(CopyOnly &&) = delete;
~CopyOnly() = default;
};

template <typename T> struct Opt {
Opt() = default;
Opt(const Opt &other) : m_value(other.m_value) {}
Opt(Opt &&other) : m_value(std::move(other.m_value)) {
// Ordinary move ctor.
// Same as =default, just writing for clarity.
}
// Ignore how `T` is actually stored to be "optional".
T m_value;
};

and this example

template <typename T> T foo(const T &t) {
auto x = t;
return x;
}

int main() {

Opt<int> opt_int;
CopyOnly copy;
Opt<CopyOnly> opt_copy;

foo(opt_int);//#1
foo(copy);//#2
foo(opt_copy);//#3
}

return x:

  1. Calls move constructor because opt_int can be moved.
  2. Calls copy constructor as a fallback because it cannot be moved.
  3. Compiler error because Opt<CopyOnly> has accessible move constructor so it is chosen, but its instantiation leads to an error due m_value(std::move(other.m_value)) trying to explicitly calls deleted move ctor.

If one disables the move constructor, copy constructor is chosen and the code is identical to #2.



Related Topics



Leave a reply



Submit