Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing a compile-time "static-if" logic for different string types in a container

I'd like to write a function template that operates on a container of strings, for example a std::vector.

I'd like to support both CString and std::wstring with the same template function.

The problem is that CString and wstring have different interfaces, for example to get the "length" of a CString, you call the GetLength() method, instead for wstring you call size() or length().

If we had a "static if" feature in C++, I could write something like:

template <typename ContainerOfStrings>
void DoSomething(const ContainerOfStrings& strings)
{
    for (const auto & s : strings)
    {
        static_if(strings::value_type is CString) 
        {
            // Use the CString interface
        }
        static_else_if(strings::value_type is wstring)
        {   
            // Use the wstring interface
        }
    }
}

Is there some template programming technique to achieve this goal with currently available C++11/14 tools?

PS
I know it's possible to write a couple of DoSomething() overloads with vector<CString> and vector<wstring>, but that's not the point of the question.
Moreover, I'd like this function template to work for any container on which you can iterate using a range-for loop.

like image 571
Mr.C64 Avatar asked Jun 03 '16 15:06

Mr.C64


4 Answers

#include <type_traits>

template <typename T, typename F>
auto static_if(std::true_type, T t, F f) { return t; }

template <typename T, typename F>
auto static_if(std::false_type, T t, F f) { return f; }

template <bool B, typename T, typename F>
auto static_if(T t, F f) { return static_if(std::integral_constant<bool, B>{}, t, f); }

template <bool B, typename T>
auto static_if(T t) { return static_if(std::integral_constant<bool, B>{}, t, [](auto&&...){}); }

Test:

template <typename ContainerOfStrings>
void DoSomething(const ContainerOfStrings& strings)
{
    for (const auto & s : strings)
    {
        static_if<std::is_same<typename ContainerOfStrings::value_type, CString>{}>
        ([&](auto& ss)
        {
            // Use the CString interface
            ss.GetLength();
        })(s);

        static_if<std::is_same<typename ContainerOfStrings::value_type, wstring>{}>
        ([&](auto& ss)
        {
            // Use the wstring interface
            ss.size();
        })(s);
    }
}

DEMO

like image 118
Piotr Skotnicki Avatar answered Nov 14 '22 06:11

Piotr Skotnicki


You could provide function overloads that do what you need:

size_t getSize(const std::string& str)
{
    return str.size();
}

size_t getSize(const CString& str)
{
    return str.GetLength();
}

template <typename ContainerOfStrings>
void DoSomething(const ContainerOfStrings& strings)
{
    for (const auto & s : strings)
    {
        ...
        auto size = getSize(s);
        ...
    }
}
like image 30
Smeeheey Avatar answered Nov 14 '22 06:11

Smeeheey


Here is one with a pretty syntax.

The goal is to get rid of the extra ()s in @Piotr's solution.

Lots of boilerplate:

template<bool b>
struct static_if_t {};
template<bool b>
struct static_else_if_t {};

struct static_unsolved_t {};

template<class Op>
struct static_solved_t {
  Op value;
  template<class...Ts>
  constexpr
  decltype(auto) operator()(Ts&&...ts) {
    return value(std::forward<Ts>(ts)...);
  }
  template<class Rhs>
  constexpr
  static_solved_t operator->*(Rhs&&)&&{
    return std::move(*this);
  }
};
template<class F>
constexpr
static_solved_t<std::decay_t<F>> static_solved(F&& f) {
  return {std::forward<F>(f)};
}

template<class F>
constexpr
auto operator->*(static_if_t<true>, F&& f) {
  return static_solved(std::forward<F>(f));
}
template<class F>
constexpr
static_unsolved_t operator->*(static_if_t<false>, F&&) {
  return {};
}
constexpr
static_if_t<true> operator->*(static_unsolved_t, static_else_if_t<true>) {
  return {};
}
constexpr
static_unsolved_t operator->*(static_unsolved_t, static_else_if_t<false>) {
  return {};
}

template<bool b>
constexpr static_if_t<b> static_if{};

template<bool b>
constexpr static_else_if_t<b> static_else_if{};

constexpr static_else_if_t<true> static_else{};

Here is what it looks like at point of use:

template <typename ContainerOfStrings>
void DoSomething(const ContainerOfStrings& strings) {
  for (const auto & s : strings)
  {
    auto op = 
    static_if<std::is_same<typename ContainerOfStrings::value_type,CString>{}>->*
    [&](auto&& s){
        // Use the CString interface
    }
    ->*static_else_if<std::is_same<typename ContainerOfStrings::value_type, std::cstring>{}>->*
    [&](auto&& s){   
        // Use the wstring interface
    };
    op(s); // fails to compile if both of the above tests fail
  }
}

with an unlimited chain of static_else_ifs supported.

It does not prevent you from doing an unlimited chain of static_else (static_else in the above is just an alias for static_else_if<true>).

like image 4
Yakk - Adam Nevraumont Avatar answered Nov 14 '22 06:11

Yakk - Adam Nevraumont


One common way to solve this is to extract the required interface out into a trait class. Something like this:

template <class S>
struct StringTraits
{
  static size_t size(const S &s) { return s.size(); }
  // More functions here
};


template <typename ContainerOfStrings>
void DoSomething(const ContainerOfStrings& strings)
{
    for (const auto & s : strings)
    {
      auto len = StringTraits<typename std::decay<decltype(s)>::type>::size(s);
    }
}


// Anyone can add their own specialisation of the traits, such as:

template <>
struct StringTraits<CString>
{
  static size_t size(const CString &s) { return s.GetLength(); }
  // More functions here
};

Of course, you can then go fancy and change the function itself to allow trait selection in addition to the type-based selection:

template <class ContainerOfStrings, class Traits = StringTraits<typename ContainerOfString::value_type>>
void DoSomething(const ContainerOfStrings& strings)
like image 4
Angew is no longer proud of SO Avatar answered Nov 14 '22 08:11

Angew is no longer proud of SO