Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to fold only part of the pack with C++17 fold expressions?

I'm trying to figure out how to fold only part of the variadic template pack with C++17 fold expressions. Suppose I'd like to create a compile time "separated string" like "str1_str2_str3_....."

It can be pretty easily done with such a code (just an example):

std::string result;

template<class... Strings>
ConcatString(Strings&... strs)
{
    (ConcatString(strs), ...);
}

template <class String>
void ConcatString(String& str)
{
     result += str + "_"
}

Then we can execute it like that

std::string s1 = "s1", s2 = "s2", s3 = "s3";
ConcatString(s1, s2, s3);
// result == "s1_s2_s3_"

As you can see there is a problem with last delimiter. Is there any way I can avoid this problem without runtime checks? One solution I can imagine is to fold only (N - 1) args and concat last one "manually".

like image 267
Dmitry Avatar asked Sep 24 '19 20:09

Dmitry


2 Answers

You could call ConcatString recursively and use constexpr if to avoid runtime checks

template<typename String, typename... Strings>
void ConcatString(String& str, Strings&... strs) {
    if constexpr(sizeof...(strs) == 0) {
        result += str;
    } else {
        result += str + "_";
        ConcatString(strs...);
    }
}
like image 104
An0num0us Avatar answered Nov 11 '22 10:11

An0num0us


Is it possible to fold only part of the pack with C++17 fold expressions?

No, a fold expression will fold over the entire pack. However, we can do some tricks to achieve what we need.

Here, your Concat function can be simplified to use a single binary fold.

template<class String, class... Strings>
std::string Concat(const std::string& delimiter, const String& str, const Strings&... strs)
{
     return (str + ... + (delimiter + strs));
}

Usage:

int main()
{
    std::cout << Concat(",", "a") << std::endl;
    std::cout << Concat(",", "a", "b") << std::endl;
    std::cout << Concat(",", "a", "b", "c") << std::endl;
}

Output:

a
a,b
a,b,c

Live Demo

The trick here is that we split the parameter pack into a singular "head" (str) and a variadic "tail" (strs). In this way we let the function parameter list pull the first element off the pack. (A lot of C++11 style template metaprogramming used this trick).

Another approach to take would be to create a set of indices 0, 1, ..., N for our parameter pack and then for our fold logic we could do something special for the 0th, Nth, or even an arbitrary element. You can find varieties of this approach on the pretty print tuple question. In C++20 thanks to template lambdas in C++20 we can move all the logic into a single method like so:

template<class... Strings>
std::string Concat(const std::string& delimiter, const Strings&... strs)
{
    return [&delimiter]<class Tup, size_t... I> (const Tup& tuple, std::index_sequence<I...>)
    {
        return (std::string{} + ... + (I == 0 ? std::get<I>(tuple) : delimiter + std::get<I>(tuple)));
    }(std::tie(strs...), std::make_index_sequence<sizeof...(strs)>{});
}

Demo 2

That ugly syntax is me creating a lambda and calling it in a single statement. You may arguably make it more readable calling it via std::invoke instead.

Note that we use a check on the index for whether to print the delimiter or not.

Let's use the index checking trick to only concat every other with the delimiter:

template<class... Strings>
std::string ConcatEveryOther(const std::string& delimiter, const Strings&... strs)
{
    return [&delimiter]<class Tup, size_t... I> (const Tup& tuple, std::index_sequence<I...>)
    {
        return (std::string{} + ... + (I % 2 == 0 ? std::get<I>(tuple) : delimiter + std::get<I>(tuple)));
    }(std::tie(strs...), std::make_index_sequence<sizeof...(strs)>{});
}

Now std::cout << ConcatEveryOther(",", "a", "b", "c", "d", "e", "f", "g") << std::endl; will give us an output like

a,bc,de,fg

Demo 3

like image 43
AndyG Avatar answered Nov 11 '22 10:11

AndyG