Try to make meta function which find method "size" in class

Viewed 108

I am trying to write function getSize() which takes some template argument, tries to find method or field in this argument and return size() or size.

my code is:

#include <iostream>
#include <vector>
#include <utility>
#include <string>
#include <type_traits>


template <typename T>
class has_size {
private:
  typedef char Yes;
  typedef Yes No[2];

  template <typename U, U> struct really_has;

  template<typename C> static Yes& Test(really_has <size_t (C::*)() const,     &C::size>*);
  template<typename C> static Yes& Test(really_has <size_t (C::*)(), &C::size>*);

  template<typename> static No& Test(...);

public:
    static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template <class T>
size_t get_size(T t){

    size_t res = 0;
    if(has_size<T>::value){

        res = t.size();
    }else{

        res = t.size;
    }


    return res;

}

int main() {
    std::vector<float> v(10);
    std::cout << std::boolalpha << has_size<std::vector<float>>::value <<     std::endl;
    std::cout << std::boolalpha << has_size<std::string>::value << std::endl;
    size_t res = get_size(v);
    std::cout<< res;
    return 0;
}

The function has_size performs rightly in my example, but when I try to call getSize I got error:

prog.cpp: In function ‘int main()’:
prog.cpp:47:24: error: the value of ‘v’ is not usable in a constant expression
  size_t res = get_size<v>;
                    ^
prog.cpp:43:21: note: ‘v’ was not declared ‘constexpr’
  std::vector<float> v(10);
                 ^
prog.cpp:47:15: error: cannot resolve overloaded function ‘get_size’ based on conversion to type ‘size_t {aka long unsigned int}’
  size_t res = get_size<v>;
           ^~~~~~~~~~~
3 Answers

So upgrading your code little bit: (for )

struct MyStruct{
    int size = 12;
};

// This function will compile only if has_size is true
template <class T,
            typename std::enable_if<has_size<T>::value, int>::type = 0>
size_t get_size(const T& t){
    return t.size();
}

// This function will compile only if has_size is FALSE (check negation !has_size)
template <class T,
            typename std::enable_if<!has_size<T>::value, int>::type = 0>
size_t get_size(const T& t){
    return t.size;
}

int main(){
    std::vector<float> v(10);
    std::cout << get_size(v) << std::endl;

    MyStruct my;
    std::cout << get_size(my) << std::endl;
    return 0;
}

Documentation about std::enable_if

So I used case #4, enabled via a template parameter.

So each case of function of get_size will exist in final program depending on enable_if result. So compiler will ignore not meeting our conditions function to compile.


So upgrading your code little bit: (from )

template <class T>
size_t get_size(const T& t){
    size_t res = 0;
    if constexpr(has_size<T>::value){
        res = t.size();
    }else{
        res = t.size;
    }
    return res;
}

int main(){
    std::vector<float> v(10);
    std::cout<< get_size(v) << std::endl;
    return 0;
}

So less code and more readable :)

This solution is using feature from C++17 if constexpr


Why your solution isn't working:

if(has_size<T>::value){ // <--- this is compile time result (has_size<T>::value) so always true or always false depends on template argument which is deduced from argument type
    res = t.size(); // this need to compile always, so if it is vector then ok if something else that doesn't have such method will fail to compile
}else{
    res = t.size; // this need to compile always, again as above
}

From smaller bugs/improvements:

  • pass by const& ;)
  • size_t res = get_size<v>; should be get_size(v) template argument will be deduced. But yeah you can write also get_size<std::vector>(v)

There's a lot going on here that needs to be fixed. To start, in your main size_t res = get_size<v>; isn't going to work, because you can't have v be a template argument, I'm assuming this was meant to be get_size(v) instead.

In get_size you have this

if (has_size<T>::value) {
    res = t.size();
} else {
    res = t.size;
}

This isn't going to work because even though only one is used, the compiler sees you doing both t.size and t.size(). I see your question is tagged c++11 so I will provide a c++11 answer.

First, I'm going to create some very simple classes to use, one with a member function and one with a data member

// using distinc values 7 and 3 to differentiate easily later
struct SizeData {
  std::size_t size = 7;
};

struct SizeFunc {
  std::size_t size() const { return 3; };
};

I'm going to also write a basic void_t for metaprogramming, and use a very standard modern metaprogramming approach to check if a given type has a .size() member function. (it looks like the technique you are attempting is outdated).

template <typename>
using void_type = void;

template <typename T, typename = void>
struct HasSizeFunc : std::false_type { };

template <typename T>
struct HasSizeFunc<T, void_type<decltype(std::declval<const T&>().size())>>
  : std::true_type { };

I can use this very easily in a main to check whether something has a .size() or not

int main() {
  std::cout << "SizeFunc: " << HasSizeFunc<SizeFunc>::value << '\n';
  std::cout << "SizeData: " << HasSizeFunc<SizeData>::value << '\n';
}

But now for the get_size() function. As I said earlier, your if/else won't work because both branches aren't compilable (if constexpr works but isn't available in c++11). So instead you can do what's called "tag dispatch" to decide which overload of a function to call in order to call the right .size

// std::size_t may not be right for every type. leaving it for simplicity.
template <typename T>
std::size_t get_size_impl(T t, std::true_type) {
  return t.size();
}
template <typename T>
std::size_t get_size_impl(T t, std::false_type) {
  return t.size;
}

template <typename T>
std::size_t get_size(T t) { // note, this should probably be a const reference
  // second argument used to select an overload of get_size_impl
  return get_size_impl(t, HasSizeFunc<T>{});
}

And to use it:

int main() {
  SizeFunc sf;
  std::cout << "SizeFunc: " << get_size(sf) << '\n';
  SizeData sd;
  std::cout << "SizeData: " << get_size(sd) << '\n';
}

click here to see all the code in one live example. I recommend watching these cppcon talks to learn more.

Also, here is what I'd do in c++17

if(has_size<T>::value){
    res = t.size();
}else{
    res = t.size;
}

these branches are evaluated at runtime. So both branches must be valid at compile time.

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }


template<class S, class...Ts>
auto select( S, Ts&&...ts )
RETURNS( std::get<S::value>(std::forward_as_tuple( std::forward<Ts>(ts)... )) )

which gives you a compile-time branch.

struct call_size_t {
  template<class T>
  auto operator()( T&& t ) const
  RETURNS( t.size() )
};
struct get_size_t {
  template<class T>
  auto operator()( T&& t ) const
  RETURNS( t.size )
};

auto f = select(has_size<T>{},
  get_size_t{},
  call_size_t{}
};
res = f(t);

this is quite annoying because you are in ; code is less than half this in and becomes trivial in .

Related