Fold expression for a parameter pack with comma operator: How to add additional parameters when expanding the pack?
Asked Answered
L

2

7

I want to design a compile-time-string class CTString that can e.g. be constructed from a parameter pack of string literals. This works using a comma-fold-expression (for this toy example, I tried to avoid the use of any system headers to make it self-contained):

template<unsigned N>
struct CTString
{
    char m_chars[N + 1U];

    template<unsigned... Ns>
    constexpr CTString(const char (&...s)[Ns])
    {
        auto* p{ m_chars };
        ((p = CopyN_(s, Ns - 1U, p)), ...);
        *p = '\0';
    }

    // copy size characters and return one past last copy:
    constexpr char* CopyN_(const char* pFrom, unsigned size, char* pTo)
    {
        for (auto i{ 0U }; i < size; ++i)
            *(pTo++) = *(pFrom++);
        return pTo;
    }
};

template<unsigned... Ns>
constexpr auto concat(const char(&...s)[Ns])
{
    return CTString<(0U + ... + (Ns - 1U))>{s...};
}

constexpr auto cHelloWorld{ concat("Hello", "World") };
static_assert(cHelloWorld.m_chars[9] == 'd');
static_assert(cHelloWorld.m_chars[10] == '\0');

Now I have an additional use case to insert a separator after each literal. How can I expand/fold the parameter pack to insert e.g. the literal "|" after each element of the pack? This is my feeble attempt that fails because the expression (s, "|")... does not work: The comma here just leads to the left operand being discarded:

template<unsigned... Ns>
constexpr auto concatWithSeparator(const char(&...s)[Ns])
{
    return CTString<(0U + ... + Ns)>{(s, "|")...};
}
// Compilation error:
constexpr auto cHelloCommaSeparated{ concatWithSeparator("Hello", "World") };

I can work around this problem by introducing a helper class and having the compile-time-string also accept packs of the helper class in its constructors. But I was wondering whether there is neat idiom that I am missing.(I did read and re-read this great article, but to no avail: C++20 idioms for parameter packs

The code that compiles is here: Godbolt Un-comment the last line to see how it fails.

Louque answered 17/4, 2023 at 10:40 Comment(5)
Do you want the result to be "Hello|World|" or "Hello|World"? The first case is easy by reusing your concat function, see here: godbolt.org/z/Gr3YYbr48Blowtorch
@joergbrech: For the time being, "Hello|World|" is all I am looking for. Your solution is very nifty and avoids the addtional helper class, I was contemplating- thanks! However, I am still intrigued to see a solution based purely on parameter pack expansion ...Louque
I don't understand: My solution uses parameter pack expansion, there is a ... in line 38 of the godbolt link. I don't think there are more parameters to expand? You create an expression of s and then expand this expression to every element of the pack. As an alternative to calling concat twice, you can just replace (s, "|") in your attempt with concat(s, "|").m_chars: godbolt.org/z/P5qcxo5vr. BTW on an unrelated note: You can make all functions consteval.Blowtorch
@joergbrech: Your solution is great! And I shall probably use it. But it exploits the fact my class happens to have a char[N+1] field, which is sort of an implementation detail. So I was wondering, whether there is a solution that expands a parameter pack (s...) to another pack (s[0], "|", s[1], "|", s[2], "|", etc.).Louque
Ah ok, I think I understand. I don't think there is a simple and easy solution. I posted an answer with the best that I could come up with.Blowtorch
B
4

A simple and pragmatic solution would be to re-use your concat function:

template<unsigned... Ns>
constexpr auto concatWithSeparator(const char(&...s)[Ns])
{
    return CTString<(0U + ... + Ns)>{concat(s, "|").m_chars...};
}

https://godbolt.org/z/hzv5qv6no.


If you want to know how to interleave the parameter pack with the seperator |, I don't think there is a simple solution. One option is to first create a tuple, whose elements are the parameter pack interleaved with the seperator:

auto tup = std::tuple_cat(
    std::tuple<const char(&)[Ns], const char(&)[2] >(s, "|")...
);

As a next step, you would need to translate the tuple back to a parameter pack, which in turn can be passed as argument(s) for the CTString constructor.

You could do this by forwarding the tuple to a templated helper function using std::index_sequence. With C++20, you have templated lambdas so that the helper function could be a lambda defined in the body of concatWithSeparator that you immediately evaluate:

template<unsigned... Ns>
constexpr auto concatWithSeparator(const char(&...s)[Ns])
{
    auto tup = std::tuple_cat(
        std::tuple<const char(&)[Ns], const char(&)[2] >(s, "|")...
    );
    return [&]<std::size_t ... Is>(std::index_sequence<Is...>)
    {
        return CTString<(0U + ... + Ns)>{std::get<Is>(tup)...};
    }
    (
        std::make_index_sequence<2*sizeof...(Ns)>{}
    );
}

https://godbolt.org/z/W1jhcrGc5


A slightly more readible version of the second solution would be to use std::apply:

template<unsigned... Ns>
constexpr auto concatWithSeparator(const char(&...s)[Ns])
{
    return std::apply(
        [&](auto const&... args){ return CTString<(0U + ... + Ns)>(args...); },
        std::tuple_cat(std::tuple<const char(&)[Ns], const char(&)[2] >{s, "|"}...)
    );
}

https://godbolt.org/z/q3GshYx5a

Blowtorch answered 17/4, 2023 at 13:2 Comment(1)
You can use std::tie to simplify creating the 2-tuples.Alard
H
1

You cannot transform a pack arg0, .., argN into arg0, sep, .., argN, sep.

As alternative, you might add extra constructor to make extra works by element of pack:

struct SeparatorTag{};

template<unsigned N>
struct CTString
{
    char m_chars[N + 1U];

    template<unsigned int NSep, unsigned... Ns>
    constexpr CTString(SeparatorTag, const char(&sep)[NSep], const char (&...s)[Ns])
    {
        auto* p{ m_chars };
        ((p = CopyN_(s, Ns - 1U, p), p = CopyN_(sep, NSep - 1, p)), ...);
        *p = '\0';
    }
    // ...
};

Demo

Hotbox answered 17/4, 2023 at 15:22 Comment(2)
Is your cannot a hunch or do you have a sort of proof? This kind of pack expansion is possible when folding with e.g. operator+: template<unsigned... Is> constexpr auto sumAndExtraOne() { return (0U + ... + (Is + 1U)); } static_assert(sumAndExtraOne<1, 2, 3, 4>() == 14U); So the comma-operator appears to be special in that respect?!Louque
You don't really interleave element in your sum, you actually have (Init @ ... @ func(Is)). With property of + (with numbers) (associativity, commutativity, ...), it behaves as if it is interleaved, but it is not.Hotbox

© 2022 - 2024 — McMap. All rights reserved.