Tuesday, September 6, 2016

Compile-time checking the existence of a class template

(Updated after a suggestion from bluescarni.) I recently had to use C++14's std::is_final but wanted to downgrade to boost::is_final if the former was not available. Trusting __cplusplus implies overlooking the fact that compilers never provide 100% support for any version of the language, and  Boost.Config is usually helpful with these matters, but, as of this writing, it does not provide any macro to check for the existence of std::is_final. It turns out the matter can be investigated with some compile-time manipulations. We first set up some helping machinery in a namespace of our own:
namespace std_is_final_exists_detail{
    
template<typename> struct is_final{};

struct helper{};

}
std_is_final_exists_detail::is_final has the same signature as the (possibly existing) std::is_final homonym, but need not implement any of the functionality since it will be used for detection only. The class helper is now used to write code directly into namespace std, as the rules of the language allow (and, in some cases, encourage) us to specialize standard class templates for our own types, like for instance with std::hash:
namespace std{

template<>
struct hash<std_is_final_exists_detail::helper>
{
  std::size_t operator()(
    const std_is_final_exists_detail::helper&)const{return 0;}
      
  static constexpr bool check()
  {
    using helper=std_is_final_exists_detail::helper;
    using namespace std_is_final_exists_detail;
    
    return
      !std::is_same<
        is_final<helper>,
        std_is_final_exists_detail::is_final<helper>>::value;
  }
};

}
operator() is defined to nominally comply with the expected semantics of std::hash specialization; it is in check that the interesting work happens. By a non-obvious but totally sensible C++ rule, the directive
using namespace std_is_final_exists_detail;
makes all the symbols of the namespace (including is_final) visible as if they were declared in the nearest namespace containing both std_is_final_exists_detail and std, that is, at global namespace level. This means that the unqualified use of is_final in
!std::is_same<
  is_final<helper>,...
resolves to std::is_final if it exists (as it is within namespace std, i.e. closer than the global level), and to std_is_final_exists_detail::is_final otherwise. We can wrap everything up in a utility class:
using std_is_final_exists=std::integral_constant<
  bool,
  std::hash<std_is_final_exists_detail::helper>::check()
>;
and check with a program
#include <iostream>

int main()
{
  std::cout<<"std_is_final_exists: "
           <<std_is_final_exists::value<<"\n";
}
that dutifully ouputs
std_is_final_exists: 0
with GCC in -std=c+11 mode and
std_is_final_exists: 1
when with -std=c+14. Clang and Visual Studio also handle this code properly.
(Updated Sep 7, 2016.) The same technique can be used to walk the last mile and implement an is_final type trait class relying on std::final but falling back to boost::is_final if the former is not present. I've slightly changed naming and used std::is_void for the specialization trick as it involves a little less typing.
#include <boost/type_traits/is_final.hpp>
#include <type_traits>

namespace my_lib{
namespace is_final_fallback{

template<typename T> using is_final=boost::is_final<T>;

struct hook{};

}}

namespace std{

template<>
struct is_void<::my_lib::is_final_fallback::hook>:
  std::false_type
{      
  template<typename T>
  static constexpr bool is_final_f()
  {
    using namespace ::my_lib::is_final_fallback;
    return is_final<T>::value;
  }
};

} /* namespace std */

namespace my_lib{

template<typename T>
struct is_final:std::integral_constant<
  bool,
  std::is_void<is_final_fallback::hook>::template is_final_f<T>()
>{};

} /* namespace mylib */

No comments:

Post a Comment