Constructor
Constructor

Reputation: 7473

Some magic with SFINAE and CRTP using clang++ and g++

Code

The following code gives different output with and without the line marked with * commented:

#include <iostream>
#include <type_traits>


template <bool>
using bool_void_t = void;

template <typename, typename = void>
struct is_complete : std::false_type
{
};

template <typename T>
struct is_complete<T, bool_void_t<sizeof(T) == sizeof(T)>> : std::true_type
{
};

template <typename Derived>
struct Base
{    
    static constexpr bool value = is_complete<Derived>{};

    // using magic = bool_void_t<value>; // *
};

struct Foo : Base<Foo>
{
};

int main()
{
    std::cout << std::boolalpha << Foo::value << std::endl;
}

Output

Compiler and its flags

In both cases clang++ 5.0.0 is used as a compiler and the compiler flags are -std=c++17 -Wall -Wextra -Werror -pedantic-errors.

More advanced research

Questions

Upvotes: 6

Views: 271

Answers (2)

Jarod42
Jarod42

Reputation: 217478

Turning out my comment into answer:

I think that is_complete make the code ill-formed (No diagnostic required)...
Its value might depends where it is instantiated and breaks ODR.

class A; // A not complete yet
static_assert(!is_complete<A>::value);
class A{}; // Now it is
static_assert(is_complete<A>::value);

From dependent_name

If the meaning of a non-dependent name changes between the definition context and the point of instantiation of a specialization of the template, the program is ill-formed, no diagnostic required. This is possible in the following situations:

  • a type used in a non-dependent name is incomplete at the point of definition but complete at the point of instantiation.

That seems to be the case for magic.

Even when magic is commented, following should also make code ill formed:

static constexpr bool value = is_complete<Derived>{};

Upvotes: 2

SJL
SJL

Reputation: 403

A common problem encountered when using CRTP is that when the base class is instantiated, the derived class is not complete. This means you cannot use member typedefs in the derived class, among other things.

This makes sense when you think about it: a template class is really a way to generate a new class type based on the given template types, so until the compiler reaches the closing } (in an approximate sense), the base class isn't fully defined. If the base class isn't fully defined, then obviously the derived class can't be either.

Because both the base and derived class are empty (in the first example), the compiler considers them complete. I would say this is incorrect, but I'm not an expect and can't be sure. Still, the trick here is that you are instantiating the value of is_complete while defining the base class. After the derived class is fully defined, it will be complete.

Also, for an example of where this matters, consider something like this:

template <typename>
class crtp_traits;

class derived;

template <>
class crtp_traits<derived>
{
public:
    using return_type = int;
};

template <typename T>
class base
{
public:
    auto get_value() const -> typename crtp_traits<T>::return_type
    {
        return static_cast<T const*>(this)->do_get_value();
    }
};

class derived : public base<derived>
{
public:
    auto do_get_value() const -> int
    {
        return 0;
    }
};

The simple solution of giving derived a member typedef using return_type = int; will not work because derived won't be complete by the time base tries to access the typedef.

Upvotes: 3

Related Questions