Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

decltype(auto) in member function ignores invalid body, decltype(expr) fails

I have a simple templated wrapper struct with a member function calling .error() on an object of its template type.

template <typename T>
struct Wrapper {
    T t;
    decltype(auto) f() {
        return t.error(); // calls .error()
    }
};

If I instantiate this with a type that doesn't have an error() member function, it's fine as long as I don't call it. This is the behavior I want.

Wrapper<int> w; // no problem here
// w.error(); // uncommented causes compilation failure

If I use what I thought was the semantic equivalent with a trailing return type, it errors on the variable declaration

template <typename T>
struct Wrapper {
    T t;
    auto f() -> decltype(t.error()) {
        return t.error();
    }
};

Wrapper<int> w; // error here

I'll accept that the two are not semantically equivalent, but is there anyway to get the behavior of the former using a trailing return type (C++11 only) without specializing the whole class with some kind of HasError tmp trickery?

like image 218
Ryan Haining Avatar asked Mar 17 '23 12:03

Ryan Haining


1 Answers

The difference between the versions

decltype(auto) f();
auto f() -> decltype(t.error());

is that the function declaration of the second can be invalid. Return type deduction for function templates happens when the definition is instantiated [dcl.spec.auto]/12. Although I could not find anything about return type deduction for member functions of class templates, I think they behave similarly.

Implicitly instantiating the class template Wrapper leads to the instantiation of the declarations, but not of the definitions of all (non-virtual) member functions [temp.inst]/1. The declaration decltype(auto) f(); has a non-deduced placeholder but is valid. On the other hand, auto f() -> decltype(t.error()); has an invalid return type for some instantiations.


A simple solution in C++11 is to postpone the determination of the return type, e.g. by turning f into a function template:

template<typename U = T>
auto f() -> decltype( std::declval<U&>().error() );

The definition of that function worries me a bit, though:

template<typename U = T>
auto f() -> decltype( std::declval<U&>().error() )
{
    return t.error();
}

For the specializations of Wrapper where t.error() is not valid, the above f is a function template that cannot produce valid specializations. This could fall under [temp.res]/8, which says that such templates are ill-formed, No Diagnostic Required:

If no valid specialization can be generated for a template, and that template is not instantiated, the template is ill-formed, no diagnostic required.

However, I suspect that rule has been introduced to allow, but not require, implementations to check for errors in non-instantiated templates. In this case, there is no programming error in the source code; the error would occur in instantiations of the class template described by the source code. Therefore, I think it should be fine.


An alternative solution is to use a fall-back return type to make the function declaration well-formed even if the definition is not (for all instantiations):

#include <type_traits>

template<typename T> struct type_is { using type = T; };

template <typename T>
struct Wrapper {
    T t;

    template<typename U=T, typename=void>
    struct error_return_type_or_void : type_is<void> {};

    template<typename U>
    struct error_return_type_or_void
        <U, decltype(std::declval<U&>().error(), void())>
    : type_is<decltype(std::declval<U&>().error())> {};

    auto f() -> typename error_return_type_or_void<>::type {
        return t.error();
    }
};
like image 142
dyp Avatar answered Apr 27 '23 08:04

dyp