Currently, I have this templated function in my codebase, which works pretty well in C 17:
/** This function returns a reference to a read-only, default-constructed
* static singleton object of type T.
*/
template <typename T> const T & GetDefaultObjectForType()
{
if constexpr (std::is_literal_type<T>::value)
{
static constexpr T _defaultObject = T();
return _defaultObject;
}
else
{
static const T _defaultObject;
return _defaultObject;
}
}
The function has two problems, though:
- It uses
if constexpr
, which means it won't compile under C 11 or C 14. - It uses
std::is_literal_type
, which is deprecated in C 17 and removed in C 20.
To avoid these problems, I'd like to rewrite this functionality to use the classic C 11-compatible SFINAE approach instead. The desired behavior is that for types that are constexpr
-constructible (e.g. int
, float
, const char *
), the constexpr
method will be called, and for types that are not (e.g. std::string
), the non-constexpr
method will be called.
Here's what I've come up with so far (based on the example shown in this answer); it compiles, but it doesn't work as desired:
#include <string>
#include <type_traits>
namespace ugly_constexpr_sfinae_details
{
template<int> struct sfinae_true : std::true_type{};
template<class T> sfinae_true<(T::T(), 0)> is_constexpr(int);
template<class> std::false_type is_constexpr(...);
template<class T> struct has_constexpr_f : decltype(is_constexpr<T>(0)){};
// constexpr version
template<typename T, typename std::enable_if<true == has_constexpr_f<T>::value, T>::type* = nullptr> const T & GetDefaultObjectForType()
{
printf("constexpr method called!\n");
static constexpr T _defaultObject = T();
return _defaultObject;
}
// non-constexpr version
template<typename T, typename std::enable_if<false == has_constexpr_f<T>::value, T>::type* = nullptr> const T & GetDefaultObjectForType()
{
printf("const method called!\n");
static const T _defaultObject = T();
return _defaultObject;
}
}
/** Returns a read-only reference to a default-constructed singleton object of the given type */
template<typename T> const T & GetDefaultObjectForType()
{
return ugly_constexpr_sfinae_details::GetDefaultObjectForType<T>();
}
int main(int, char **)
{
const int & defaultInt = GetDefaultObjectForType<int>(); // should call the constexpr function in the namespace
const float & defaultFloat = GetDefaultObjectForType<float>(); // should call the constexpr function in the namespace
const std::string & defaultString = GetDefaultObjectForType<std::string>(); // should call the non-constexpr function in the namespace
return 0;
}
When I run the program above, here is the output I see it print to stdout:
$ ./a.out
const method called!
const method called!
const method called!
... but the output I would like it to emit is this:
$ ./a.out
constexpr method called!
constexpr method called!
const method called!
Can anyone point out what I'm doing wrong? (I apologize if it's something obvious; SFINAE logic is not a concept that comes naturally to me :/ )
CodePudding user response:
As @Igor Tandetnik mentions in comments, static const T _defaultObject{};
works in both cases and performs compile-time initialization when possible. There's no need for constexpr
.
N3337 [basic.start.init]:
Constant initialization is performed:
- [...]
- if an object with static or thread storage duration is initialized by a constructor call, if the constructor is a constexpr constructor, if all constructor arguments are constant expressions (including conversions), and if, after function invocation substitution ([dcl.constexpr]), every constructor call and full-expression in the mem-initializers and in the brace-or-equal-initializers for non-static data members is a constant expression;
- if an object with static or thread storage duration is not initialized by a constructor call and if every full-expression that appears in its initializer is a constant expression.