Conditional compilation of templates
Asked Answered
S

2

0

I am trying to get static_assert to help me avoid null pointers in C++11.

The problem seems to be that C++11 require the compiler to compile templates even if they are not instantiated.

I have the following code:

#include <type_traits>

template<typename T, typename... Us>
std::enable_if_t< std::is_constructible<T, Us...>::value == true, T * >
create_if_constructible(Us... args) { return new T(args...); }

template<typename T, typename... Us>
std::enable_if_t< std::is_constructible<T, Us...>::value == false, T * >
create_if_constructible(Us... args) { 
   static_assert( false, "Class T constructor does not match argument list.");
   return nullptr; 
}

struct ClassA {
   ClassA(int a, string b) {}
};

void foo() {
   ClassA *a = create_if_constructible<ClassA>(1, "Hello");
   // ClassA *b = create_if_constructible<ClassA>(1, "Hello", "world"); // I want compile time error here.
}

I would like this to compile without error. But the static_assert is compiled and gives me a compile time error.

Only if the the second instantiation of the ClassA is in the code should it give me a compile time error.

Shitty answered 6/4, 2015 at 9:40 Comment(12)
I'm confused - even without create_if_constructible you get a compiler error if you provide the wrong number of arguments to the ClassA constructor... how's what you're doing leading to "helping [you] avoid null pointers"?Dael
Anyway... if you change your static_assert() inside the "false" version to static_assert(std::is_constructible<T, Us...>::value, "Class T constructor does not match argument list."); it will do what you seem to want....Dael
@TonyD I believe that will still run afoul of the "no valid specialization can be generated" rule and so is still ill-formed no diagnostic required (there's no set of template arguments that will make the template signature valid and yet not trigger the static_assert). A workaround is straight forward though - template<class> class always_false : std::false_type {}; and static_assert on always_false<T>::value. But I'm also not seeing the point of this code.Cavorelievo
@T.C.: you're referring to 14.6/8? I don't see why it would apply, and the text doesn't make it clear to me that there needs to be a condition where the static_assert doesn't trigger. It seems intuitive to me that the template should be selected based on signature, then the static_assert kick in. Could you share your understanding of the relevance? Thanks.Dael
@TonyD The problem is in the enable_if considered together with is_constructible - regardless of the value of is_constructible<...>, either the return type is invalid (if it's true), or the static assert fires (if it's false). As a result, there's no way you can generate a valid specialization for it.Cavorelievo
@T.C.: 14.6/8 is specifically a check on "the syntax of every template" - given the syntax of is_constructible<...>'s use is correct, it doesn't seem to me to be valid for the compiler to deem there to be "no valid specialisation" until instantiation, when the actual compile-time-constant value of is_constructible is considered. Under 14.6/8's "Note": If a template is instantiated, errors will be diagnosed according to the other rules in this Standard. Exactly when these errors are diagnosed is a quality of implementation issue." I'd expect this to be one of these instantiation errorsDael
@TonyD static_assert(false, ""); is perfectly valid syntatically.Cavorelievo
@Cavorelievo true - so is it a compiler bug or my misreading... odds aren't looking good for me O_o.Dael
@TonyD Note that the example immediately following that paragraph permits p = i; to be diagnosed for int i and char *p, though p = i; is also syntactically valid. Knowing which names are type names allows checking of syntax (and without the ability to check syntax you can't check semantic constraints), but the permission to diagnose is broader.Cavorelievo
@Cavorelievo Another angle on this is - regardless of the syntax-checking-only question re 14.6/8, is not std::is_constructible<T, Us...>::value - being a T-dependent value - required to defer evaluation until instantiation under Two-Phase Name Lookup rules? That could be considered to prohibit the comparison of enable_if condition with static_assert condition that leads to the insight that no valid specialisation is possible. (FWIW, I'll point out that it does happen to work on the GCC compiler I tried, though that's just one sample point).Dael
@TonyD I'm aware of no rule in the standard that requires it to be deferred. std::is_constructible<T, Us...>::value is a dependent name, to be sure, but that doesn't require deferral of checking.Cavorelievo
@TonyD I can think of one argument against this type of checking in the general case - if std::is_constructible<T, Us...>::value is some special type rather thanbool, then given sufficiently devious operator overloads you can have a case that would result in a valid specialization. However, a sufficiently smart compiler can ignore this possibility in this case because specializing is_constructible is UB. Considering the implementation costs, it's unlikely that we'll see a compiler doing this any time soon, but it's always better to write standard-conforming code.Cavorelievo
C
3

The standard permits, but does not require, compilers to diagnose templates for which no valid instantiation can be generated. This can range from simple syntax errors to your example of a constant false expression in a static_assert. §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.

I'm rather baffled by all this SFINAE machinery, though. A simple

template<typename T, typename... Us>
T* create_if_constructible(Us... args) { return new T(args...); }

already refuses to compile if T is not constructible from the parameter given, so I'm not sure how this complex circumlocution will help you "avoid null pointers".

Regardless, a simple way to make choosing the second function template a compile-time error is to explicitly delete it.

template<typename T, typename... Us>
std::enable_if_t< std::is_constructible<T, Us...>::value == false, T * >
create_if_constructible(Us... args) = delete;

Alternatively, if you are partial to static_asserts, perhaps because of the custom error message, you must ensure that there is theoretically a way to generate a valid instantiation of your template. That means that 1) what you are static_asserting on must depend on a template argument, and 2) there must be theoretically a way for the condition to be true. A simple way is to use an auxiliary template:

template<class> class always_false : std::false_type {};

template<typename T, typename... Us>
std::enable_if_t< std::is_constructible<T, Us...>::value == false, T * >
create_if_constructible(Us... args) { 
   static_assert( always_false<T>::value, "Class T constructor does not match argument list.");
   return nullptr; 
}

The key point here is that the compiler cannot assume that always_false<T>::value is always false because it is always possible that there's a specialization later that sets it to true, and so it is not allowed to reject this at template definition time.

Cavorelievo answered 6/4, 2015 at 11:9 Comment(1)
I agree with your comments on my post and then I've decided to remove it and +1 yours. However, for the benefit of others, I would like to leave the best part of it which is this reference on two-phase name lookup.Seaward
P
0

In all C++ standards ever, templates are compiled in two phases. The second phase is instantiation, but compilation can also fail in phase 1. In particular, syntax errors are detected in phase 1.

In your case, the simpler solution is to leave out the body of the second instantiation.

Another solution is to use T in the static_assert, so the compiler must delay evaluation to phase 2. Trivially: static_assert(sizeof(T)==0,

Parclose answered 6/4, 2015 at 10:57 Comment(9)
static_assert(sizeof(T)==0, /*... */); is still ill-formed NDR.Cavorelievo
Commpailer complains when leaving out the body of the function. And sizeof(T)==0 still evaluates the funciton even though it is not used.Shitty
@T.C.: How is that ill-formed?Parclose
@user2099460: sizeof isn't a function, and does not evaluate it's argument. sizeof(*(int*)nullptr) is perfectly legal.Parclose
@Parclose Because there is no T for which that assertion will not fire.Cavorelievo
@T.C.: Sure, but that's irrelevant. The expression is syntactically valid and furthermore the whole point of static_assert(false, "diagnostic") is that a diagnostic is required.Parclose
A template for which no valid specialization can be generated is ill-formed by itself, effectively meaning that the static_assert can still fire at template definition time with a sufficiently smart compiler.Cavorelievo
@T.C.: I think you may be right. This answer explains why the technique is basically sound, but the subtle difference is that it involves specializations whereas this involves overloading.Parclose
@Parclose I wrote that answer :) Also, that one uses a sizeof...(Pack), which can validly be zero (for an empty pack), not plain sizeof, which can't.Cavorelievo

© 2022 - 2024 — McMap. All rights reserved.