Search code examples
c++templatesc++11template-meta-programmingllvm-clang

Invalid template instantation and the metaprogram compiles fine?


I was working on a simple solution to the common "Conditional on ill-formed types" problem (Like this yesterday question).

In my codebase I have a template to hold uninstanced templates and instance them later. Something like this:

template<template<typename...> class F>
struct lazy
{};

namespace impl
{
    template<typename L , typename... ARGS>
    struct lazy_instance;

    template<template<typename...> class F , typename... ARGS>
    struct lazy_instance<lazy<F>,ARGS...> : public identity<F<ARGS...>>
    {};
}

template<typename L , typename... ARGS>
using lazy_instance = typename impl::lazy_instance<L,ARGS...>::type;

Where identity is the identity metafunction.
This could be used as follows:

using vector = lazy<std::vector>;
using int_vector = lazy_instance<vector,int>;

So the solution which comes to my mind is to wrap the two targets of the conditional on that way, and instantiate the result of the conditional, so only the selected template is instanced. For that purpose, I have modified lazy and impl::lazy_instance to allow us to pass the template parameters at the lazy instantation point:

template<template<typename...> class F , typename... ARGS>
struct lazy
{};

namespace impl
{
    template<typename L , typename... ARGS>
    struct lazy_instance;

    template<template<typename...> class F , typename... ARGS , typename... IARGS>
    struct lazy_instance<lazy<F,ARGS...>,IARGS...> : public identity<F<ARGS...>>
    {};
}

Note that in this case lazy_instance takes template parameters too, but they are ignored. I have donde this on that way to have the same interface for both usage cases.

So our main problem, the evaluation of conditional selection of potentially ill-formed types could be achieved as follows:

using ok = lazy_instance<typename std::conditional<true,
                                                   lazy<foo,int>,
                                                   lazy<foo,bool>
                                                  >::type
                        >;

Where foo is a template where the bool instantation is ill-formed, for example:

template<typename T>
struct foo;

template<>
struct foo<int>
{};

It seems to work, isn't? Great. But a couple of minutes later I changed the boolean flag to false, and surprisingly it works too! Even if foo<bool> specialization is not defined!

Also the metaprogram has a static_assert bellow to check if the evaluation of the conditional was successfull:

static_assert( std::is_same<ok,foo<int>>::value , "Mmmmm..." );

When I changed the condition from true to false, I changed the test to see whats happening there:

static_assert( std::is_same<ok,foo<bool>>::value , "Mmmmm..." );

And the metaprogram passes the test too! Even with the explicit instantation of foo<bool>, and ok, which should be an alias to that instance.
Finally I have checked that there is no "No matching specialization for foo<bool>" until I declare a variable of type ok: ok anok;

I'm working with CLang 3.4, on C++11 mode (-std=c++11). My question is: Whats happening here? Is that behaviour correct or its a LLVM bug?

EDIT: Here is a SSCCE running at ideone. It uses GCC 4.8.1, but seems like it has the same behaviour.


Solution

  • Long story; short.

    You are actually not instantiating foo<bool> in the following expression:

    std::is_same<ok, foo<bool>>::value;
    

    When does implicit instantiation occur?

    14.7.1 Implicit instantiation [templ.inst]

    5) A class template specialization is implicitly instantiated if the class type is used in a context that requires a completely-defined object type or if the completeness of the class type might affect the semantics of the program.

    7) If an implicit instantiation of a class template specialization is required and the template is declared but not defined, the program is ill-formed.

    What the above text says is that a class template is only implicitly instantiated when it is used in a context that requires it to be fully defined, such as when declaring an object of said template, or when trying to access something inside it.

    Checking if one type is the same as another does not count as such context, since we are merely comparing two names.


    When is a completely-defined object required?

    The Standard makes a reference to "completely-defined" in several different locations, mostly when it explicitly says that such an object is required.

    The easiest definition of when we need a completely-defined object is by reading the following, which explains what it isn't.

    3.9p5 Types [basic.types]

    A class that has been declared but not defined, or an array of unknown size or of incomplete element type, is an incompletely defined object type. Incompletely-defined object types and the void types are incomplete types (3.9.1). Objects shall not be defined to have an incomplete type.

    The wording above states that as long as we don't declare an object to be of an incomplete-type, we are in the clear; ie. our template will not be implicitly instantiated.

    See the below example where (C) and (D) tries to create an object of incomplete-type, both (A) and (B) are legal since they don't cause implicit instantiation.

    template<class T> struct A;
    
    typedef A<int> A_int; // (A), legal
    A<int> *     ptr;     // (B), legal
    A<int>       foo;     // (C), ill-formed; trying to declare an object of incomplete-type
    A<int>::type baz;     // (D), ill-formed; trying to reach into the definition of `A<int>`