Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

  • The line marked with * is commented: true.
  • The line marked with * is not commented: false.

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

  • clang++ 5.0.0 (-Wall -Wextra -Werror -pedantic-errors)

                         -std=c++14   -std=c++17
    * is commented       false        true
    * is not commented   false        false
    
  • g++ 7.2.1 (-Wall -Wextra -Werror -pedantic-errors)

                         -std=c++14   -std=c++17
    * is commented       true         true
    * is not commented   false        false
    

Questions

  • Is such behavior of the compilers standard compliant? If yes, what rationale stands behind it?
  • What differences between C++14 and C++17 could lead to the difference in the observed behavior using the clang++ compiler when the line marked with * is commented (the output is false with the -std=c++14 compiler flag and true with the -std=c++17 one)?
like image 962
Constructor Avatar asked Feb 06 '18 14:02

Constructor


2 Answers

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.

like image 105
SJL Avatar answered Oct 27 '22 23:10

SJL


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>{};
like image 2
Jarod42 Avatar answered Oct 27 '22 23:10

Jarod42