0

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:

  1. It uses if constexpr, which means it won't compile under C++11 or C++14.
  2. 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 :/ )

cpplearner
  • 13,776
  • 2
  • 47
  • 72
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • 1
    Wouldn't `static const T _defaultObject{};` work in both cases, so that you wouldn't need to differentiate between literal and non-literal types in the first place? – Igor Tandetnik Nov 21 '22 at 04:28
  • @IgorTandetnik it does work, but I want to allow the singleton-objects to be fully initialized at compile-time when possible (as opposed to being demand-initialized on the first call to the function, which requires the compiler to emit code that locks a mutex in order to ensure thread-safe initialization of the object) – Jeremy Friesner Nov 21 '22 at 05:00
  • 1
    For what value of `T` does your approach achieve that while mine does not? – Igor Tandetnik Nov 21 '22 at 05:04
  • @IgorTandetnik that's a fair question -- I thought that in my earlier testing I had identified that the compiler would not optimize away the mutex-guard for simple types, but when I test again now, I see that it does. This is complicated by the fact that I updated the XCode install on my Mac from 13.2 to 14.1 last night, so now I'm not certain whether I simply misread the compiler output before, or if the newer compiler version has improved behavior. Do you think it's reasonable to expect this optimization to "just work" without SFINAE magic? If so, I will happily delete this question :) – Jeremy Friesner Nov 21 '22 at 05:25

1 Answers1

1

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.
cpplearner
  • 13,776
  • 2
  • 47
  • 72