Wednesday, 30 October 2013

Some basic decltype-SFINAE


In this blog I'd like to give you the impression that I'd know everything ;-), but, alas, that's not true. Mainly because with the advent of C++11 there's a lot of things for me to learn. You may say I'm late to the party, and yes, I was somehow lazy playing (or rather not playing:) with C++11 compilers. That was because my clients didn't want to use the newest features yet (you know, a bunch of conservative ... etc.). But now, all of a sudden, I can use every C++11 and Boost feature I want in my daily work (yay!!!), and there's definitively a difference between playing with something and using it on a daily basis!

Back to the things I should learn. There are many of them, but we'll take the decltype-SFINAE as an example. The what-SFINAE? Or: the decltype-what?? Or, even worse: the what-what???

So what is SFINAE really? You can google it, or you can just take my word that SFINAE works by purposely introducing compile errors (thus the SF part in the name) which will be then ignored by compiler, (the INAE part in the name) just to remove one of template specializations to suit your purpose!

The idea is that for one class you'll have one set of template definitions (where the unwanted ones will be ignored by compiler), and for an another class a different one. Simple, isn't it? ( BTW as I'm getting older and older I came to recognize that most of the human ideas are simple, but some of them just aren't clearly stated; that seems to be an ego-thing for some people...)

So what is the decltype-SFINAE? Simple again, it's using SFINAE in the C++11 auto+decltype syntax for function declarations. Like here:
  template <class T> auto xxx() -> decltype(XXX, string) { ... }
How's that? Remember, decltype takes an expression as argument, and comma operator in expressions renders the last part as the value, so everything before the comma (i.e. the XXX thing) can be used to fool the compiler into/out of SFINAE. Tada!

When I first saw this technique in action*, used for detecting if some method is defined in a given class, there was an utility test class defined:
  template<typename T> struct has_size_method 
  {  
    //...

    template&ltclass C> static yes test(SFINAE<C,&C::size>*); // won't explain this impl.!

  public:
    static constexpr bool value = std::is_same<decltype(test<T>(nullptr)),yes>::value;
  };
and then it could be used like that:
    cout << has_size_method<ClassXXX>::value;
I wanted the same functionality (i.e. detection of a special optional method) but I didn't want another utility class. Frankly speaking, I'm more for the direct approach, not for burying simple concept under irritating levels of indirections. I a word, there had to be a better way! So I tried this:
  // got get_channel_id()?
  template <typename T, typename U>
  auto mk_object_name(T* obj, U prefix) 
    -> decltype(std::declval<T>().get_channel_id() == 1, std::string()) 
  {            
    return format_strg(prefix * 1000 + obj->get_channel_id(), std::hex, typeid(*obj).name());
  }
  
  // nope!
  template <typename T, typename U>
  std::string mk_object_name(T* obj, U prefix, T* obj2 = nullptr) 
  {
    return format_strg(prefix, std::hex, typeid(*obj).name());
  }
The first overload will be removed by SFINAE if the type T doesn't have the get_channel_id() method, the second one will be always present in the normal case, and let's hope that it's sufficiently different for the first one. Simple, isn't it? Then I used it like this:
  struct XXX {
    int get_channel_id() { return 1; } 
  };
  struct YYY {
    int get_YYY_id() { return 1; } 
  };

  main()
  {
    XXX x;    
    std::string nameX = mk_object_name(&x, 100);
    std::cout << nameX << std::endl;

    YYY y;    
    std::string nameY = mk_object_name(&y, 200);
    std::cout << nameY << std::endl;
 }
Alas, it didn't work for the XXX struct. That is what comes from hoping that the templates will just match! The obvious problem here is, that in this case both overloads are enabled. You did spot it at once? Sorry, I said this is a post about basic(!) decltype-SFINAE! So what can we do to hide the base version if the extended version is possible? A template function for dispatching to the desired overload proved to be sufficient:
  // actual functions
  template <typename T, typename U>
  auto mk_object_name_helper(T* obj, U prefix, int) 
    -> decltype(std::declval<T>().get_channel_id() == 1, std::string()) 
  {            
    return format_strg(prefix * 1000 + obj->get_channel_id(), std::hex, typeid(*obj).name());
  }

  template <typename T, typename U>
  std::string mk_object_name_helper(T* obj, U prefix, long) 
  {
    return format_strg(prefix, std::hex, typeid(*obj).name());
  }

  // "dispatching" function
  template <typename T, typename U>
  std::string mk_object_name(T* obj, U prefix) 
  {
    return mk_object_name_helper(obj, prefix, 0);
  }
And then it worked like a charm**. I think, it's not too much overhead when compared to the has_size_method template shown above. The dispatching function just uses the fact that int is a better overload for zero than long.

You can compile the above code online, for example at http://gcc.godbolt.org/ use g++-4.8 with the -std=c++11 option. The nice thing there is that in the assembler window you can see what template overload was selected. Additionally you'll see the famous template bloat in action! It's fun! If you'd only like to let it run online, try http://liveworkspace.org/.

PS: And what was it all for? Did you get it? We were generating object names for various "software device" types, some supporting only the board IDs, some offering additional support for channels.

--
* look for example here: http://dev.krzaq.cc/checking-whether-a-class-has-a-member-function-with-a-given-signature/

** There's another way, of course! Facebook's Folly library accomplishes it with enable_if and the pair of is_same and !is_same in the definition of the return value of a function:
  template <class T>
  typename std::enable_if<
    HasLockUnlock<T>::value && !std::is_same<T, boost::shared_mutex>::value>::type
  acquireRead(T& mutex) {
    mutex.lock();
  }
  //Special case for boost::shared_mutex.
  template <class T>
  typename std::enable_if<std::is_same<T, boost::shared_mutex>::value>::type
  acquireRead(T& mutex) {
    mutex.lock_shared();
  }
 Look here for overview of how the classic enable_if<> can be used. 

No comments:

Post a Comment