Custom compile error message when undefined subtype is accessed
Asked Answered
D

2

5

I have some types which have sub-types with the same name each:

struct TypeA {
    typedef int subtype;
};
struct TypeB {
    typedef float subtype;
};

and also types which don't have this sub-type but which are used in the same context:

struct TypeC {
    // (no subtype defined)
};

How can I add a dummy sub-type which gives a custom compile error message?

My (so far unsuccessful) attempt is:

struct TypeC {
    struct subtype {
        static_assert(false, "Attempt to access the non-existent subtype of TypeC.");
    };
};

But static_assert(false, ...) can't work, as the compiler throws the error even if the type is never accessed.

How can I delay the evaluation of static_assert to the time when the type is being accessed?

A failed attempt is to introduce a dummy enum and construct an expression out of it:

enum { X };
static_assert(X != X, "...");

Concrete use case: I have a class-template List which is defined with the sub-types head and tail if non-empty, and should give an error if these sub-types are used if it is empty:

template<typename...>
struct List;

// empty list:
template<>
struct List<> {
    struct head { static_assert(false, "Attempt to access the head of an empty list."); };
    struct tail { static_assert(false, "Attempt to access the tail of an empty list."); };
};

// non-empty list:
template<typename Head, typename ...Tail>
struct List<Head, Tail...> {
    typedef Head head;
    typedef List<Tail...> tail;
};

If I simply leave out the types head and tail, when accessing e.g. the 3rd element of a list which has size 2 with the code List<int,int>::tail::tail::head gives the not so nice message (g++ 4.7.2): 'head' is not a member of 'List<int>::tail {aka List<>}'

Denver answered 25/6, 2014 at 23:42 Comment(4)
That List<> example doesn't complain about the static_asserts? I thought the constant expression needed to involve a template parameter to avoid immediate evaluation.Procambium
Hmm, the dummy enum doesn't seem to work either.Kind
@Procambium It works with g++ 4.7.2, not sure about other compilers or even the standard.Denver
Ok then I will edit and name this a "failed attempt" :)Denver
K
5
// empty list:
template<typename... Args>
struct List {
    struct head {static_assert(sizeof...(Args) != 0, "Attempt to access the head of an empty list."); };
    struct tail {static_assert(sizeof...(Args) != 0, "Attempt to access the tail of an empty list."); };
};

// non-empty list:
template<typename Head, typename ...Tail>
struct List<Head, Tail...> {
    typedef Head head;
    typedef List<Tail...> tail;
};

Edit: This problem actually touches on three aspects of how C++ templates work:

  1. (§14.7.1 [temp.inst]/p1) Unless a class template specialization has been explicitly instantiated (14.7.2) or explicitly specialized (14.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program. The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions ... of the class member functions, member classes, [...].
  2. (§14.7.1 [temp.inst]/p11) An implementation shall not implicitly instantiate ... a member class...of a class template that does not require instantiation.
  3. (§14.6 [temp.res]/p8) If no valid specialization can be generated for a template, and that template is not instantiated, the template is ill-formed, no diagnostic required.

3) means that the static_assert expression must depend on a template argument, as otherwise "no valid specialization" can be generated for the template and the program is ill-formed, and compilers are free to report an error (although they don't have to). In the above code, a valid specialization can be generated for the first template, but such a specialization is never used because of the partial specialization.

The solution given above also relies on 1) and 2). 1) provides that implicitly instantiating a template specialization only instantiates the declarations (not definitions) of member classes, and 2) means that compilers are affirmatively prohibited from attempting to instantiate head or tail if one is merely using an implicitly instantiated List<>. Note that this rule does not apply if you explicitly instantiate List<> with template struct List<>;.

The solution in leemes's answer works because typedefs do not require a complete type and so do not trigger implicit instantiation of SubTypeErrorMessage<> under 1), and the use of a template argument in the static_assert in SubTypeErrorMessage bypasses 3), as a valid specialization (i.e., SubTypeErrorMessage<true>) can be generated for that template.

It's worth noting that in both cases the instantiation rules mean that it's still legal to use List<>::head or TypeC::subtype as long as you don't use them in a way that requires a complete type. Thus something like int f(List<>::head & ) { return 0; } is valid, though entirely meaningless since there's no way you can actually call that function. If you don't define List<>::head at all, however, the compiler will report a (perhaps cryptic) error on this code. So that's the trade-off for prettier error messages :)

Kind answered 26/6, 2014 at 0:6 Comment(3)
Oh nice one, there is even the "correct logic" in the expression ;) (although cases with a size greater 0 never reaches it...) Thank you for this nice solution.Denver
Thank you very much for your detailed explanation. So summarizing, both your and my solution are guaranteed to work, i.e. the compiler is not allowed to evaluate the assertion earlier than I'd like it to be?Denver
@Denver I believe so.Kind
D
5

To delay the evaluation of static_assert to the point where your type is accessed, you have to make the expression depend on a template parameter.

One possible solution is to add a helper class template just for printing the error message conditionally (depending on the value of the template parameter):

template<bool X = false>
struct SubTypeErrorMessage {
    static_assert(X, "Attempt to access the non-existent subtype of TypeC.");
};

Then, in the concrete type where you want to have a "dummy sub-type":

struct TypeC {
    typedef SubTypeErrorMessage<> subtype;
};

Live Demo

Denver answered 26/6, 2014 at 0:2 Comment(0)
K
5
// empty list:
template<typename... Args>
struct List {
    struct head {static_assert(sizeof...(Args) != 0, "Attempt to access the head of an empty list."); };
    struct tail {static_assert(sizeof...(Args) != 0, "Attempt to access the tail of an empty list."); };
};

// non-empty list:
template<typename Head, typename ...Tail>
struct List<Head, Tail...> {
    typedef Head head;
    typedef List<Tail...> tail;
};

Edit: This problem actually touches on three aspects of how C++ templates work:

  1. (§14.7.1 [temp.inst]/p1) Unless a class template specialization has been explicitly instantiated (14.7.2) or explicitly specialized (14.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program. The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions ... of the class member functions, member classes, [...].
  2. (§14.7.1 [temp.inst]/p11) An implementation shall not implicitly instantiate ... a member class...of a class template that does not require instantiation.
  3. (§14.6 [temp.res]/p8) If no valid specialization can be generated for a template, and that template is not instantiated, the template is ill-formed, no diagnostic required.

3) means that the static_assert expression must depend on a template argument, as otherwise "no valid specialization" can be generated for the template and the program is ill-formed, and compilers are free to report an error (although they don't have to). In the above code, a valid specialization can be generated for the first template, but such a specialization is never used because of the partial specialization.

The solution given above also relies on 1) and 2). 1) provides that implicitly instantiating a template specialization only instantiates the declarations (not definitions) of member classes, and 2) means that compilers are affirmatively prohibited from attempting to instantiate head or tail if one is merely using an implicitly instantiated List<>. Note that this rule does not apply if you explicitly instantiate List<> with template struct List<>;.

The solution in leemes's answer works because typedefs do not require a complete type and so do not trigger implicit instantiation of SubTypeErrorMessage<> under 1), and the use of a template argument in the static_assert in SubTypeErrorMessage bypasses 3), as a valid specialization (i.e., SubTypeErrorMessage<true>) can be generated for that template.

It's worth noting that in both cases the instantiation rules mean that it's still legal to use List<>::head or TypeC::subtype as long as you don't use them in a way that requires a complete type. Thus something like int f(List<>::head & ) { return 0; } is valid, though entirely meaningless since there's no way you can actually call that function. If you don't define List<>::head at all, however, the compiler will report a (perhaps cryptic) error on this code. So that's the trade-off for prettier error messages :)

Kind answered 26/6, 2014 at 0:6 Comment(3)
Oh nice one, there is even the "correct logic" in the expression ;) (although cases with a size greater 0 never reaches it...) Thank you for this nice solution.Denver
Thank you very much for your detailed explanation. So summarizing, both your and my solution are guaranteed to work, i.e. the compiler is not allowed to evaluate the assertion earlier than I'd like it to be?Denver
@Denver I believe so.Kind

© 2022 - 2024 — McMap. All rights reserved.