Special behavior for decltype of call operator for incomplete types
Asked Answered
D

1

7

I've been struggling with a compilation issue, and have been able to shrink the problem down to a small code segment.

To set the stage, I'm trying to do CRTP, where the base method calls another in the derived class. The complication is, I want to use trailing return types to get the type of forwarding directly to the the Derived class's method. This always fails to compile unless I forward to the call operator in the derived class.

This compiles:

#include <utility>

struct Incomplete;

template <typename Blah>
struct Base
{
    template <typename... Args>
    auto entry(Args&&... args)
        -> decltype(std::declval<Blah&>()(std::declval<Args&&>()...));
};

void example()
{
    Base<Incomplete> derived;
}

While this does not: (note comment for the only difference)

#include <utility>

struct Incomplete;

template <typename Blah>
struct Base
{
    template <typename... Args>
    auto entry(Args&&... args)
        -> decltype(std::declval<Blah&>().operator()(std::declval<Args&&>()...));
        //             I only added this ^^^^^^^^^^^
};

void example()
{
    Base<Incomplete> derived;
}

The error I get:

<source>: In instantiation of 'struct Base<Incomplete>':
15 : <source>:15:22:   required from here
10 : <source>:10:58: error: invalid use of incomplete type 'struct Incomplete'
         -> decltype(std::declval<Blah&>().operator()(std::declval<Args&&>()...));
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^

There seems to be some special behavior going on during resolution of the decltype in the Derived class. Is there something in the standard that would explain this?

EDIT: Made an even bigger simplification

PS: compiling example on godbolt: https://godbolt.org/g/St2gYC

Dillard answered 5/8, 2017 at 6:15 Comment(3)
Why are you not writing std::declval<Blah>()? I'd guess that the second block is an odr use of Incomplete while the first is not.Tartuffery
@PasserBy Either way, it's the same problem. I changed it to your suggestion so there's less confusion.Dillard
Are you looking for this maybe? #7944025Riding
C
5

Instantiating a class template instantiates the declarations of its member function templates ([temp.inst]/2). I.e. we're looking at the declaration

template <typename... Args>
auto entry(Args&&... args)
    -> decltype(std::declval<Incomplete&>().operator()(std::declval<Args&&>()...));

Now consider [temp.res]/10:

If a name does not depend on a template-parameter (as defined in 14.6.2), a declaration (or set of declarations) for that name shall be in scope at the point where the name appears in the template definition;

And indeed, the name operator() does not depend on a template parameter. It's neither type- nor value-dependent, and it's not a dependent name, either. Clearly, there is no declaration in scope, hence the declaration is ill-formed, no diagnostic required.

By contrast, your first snippet does not necessitate the lookup of a name within Incomplete. The transformation of x(...), where x is of class type, to x.operator()(...) happens only after operator() is looked up within x—[over.call]:

Thus, a call x(arg1,...) is interpreted as x.operator()(arg1, ...) for a class object x of type T if T​::​operator()(T1, T2, T3) exists and if the operator is selected as the best match function by the overload resolution mechanism ([over.match.best]).

This is different from the paragraph that made your second code ill-formed: [temp.res]/10 says that some declaration(s) must be in scope, and that the name is bound to those declarations. The above transformation requires that the argument types (and also the number...) are known such that we can uniquely determine one operator() to be called; that is, we don't just insert .operator(), but always simultaneously identify which operator function is called. We can find further confirmation of this interpretation in [temp.dep]:

If an operand of an operator is a type-dependent expression, the operator also denotes a dependent name. Such names are unbound and are looked up at the point of the template instantiation [...]

The operator()'s argument operands are clearly type-dependent.

Cibis answered 7/8, 2017 at 11:37 Comment(1)
Awesome! Perfect explanation. As a side-note the way I got around this issue is to force the operator() to be dependent on a template-parameter. First introduce another declval template <typename T, typename...> T mydeclval(); and then use it like so decltype(mydeclval<Incomplete, Args...>().operator(std::declval<Args&&>()...))Dillard

© 2022 - 2024 — McMap. All rights reserved.