Home > Mobile >  Fold expression: Replacing specific type but forwarding all others, how to correctly specialise `std
Fold expression: Replacing specific type but forwarding all others, how to correctly specialise `std

Time:07-08

I've trying to replace a specific type within a fold expression while simply forwarding all other types, but failed miserably.

A simulation of std::forward; actually copied GCC's implementation of and just added a bit of output to see what's going on:

namespace test
{

template<typename T>
T&& fw(typename std::remove_reference<T>::type& t) noexcept
{
    std::cout << "standard" << std::endl;
    return static_cast<T&&>(t);
}

template<typename T>
T&& fw(typename std::remove_reference<T>::type&& t) noexcept
{
    std::cout << "standard (r-value)" << std::endl;
    static_assert
    (
            !std::is_lvalue_reference<T>::value,
            "template argument substituting T is an lvalue reference type"
    );
    return static_cast<T&&>(t);
}

}

As std::forward requires explicit template specialisation I tried providing another set of templated overloads for, but these haven't been considered for overload resolution and would, if that had worked, have led to ambiguous function calls anyway.

So I tried specialising the forwarding templates (well aware that specialising std::forward itself would, as in a header, have lead to replacements where they shouldn't occur) – let's say for std::string. Still, out of pure curiosity (I discovered actually not needing the variadic template thus could work with constexpr if's inside the function instead, so no XY-problem – any more), how would I do so?

This attempt, trying to give an overload for every possible reference type (ambiguities might be solved by dropping one of) failed miserably:

namespace test
{

template<>
std::string const& fw<std::string const>(std::string const& t) noexcept
{
    std::cout << "specialised const" << std::endl;
    return t;
}
#if 0
template<>
std::string& fw<std::string>(std::string& t) noexcept
{
    std::cout << "specialised non-const" << std::endl;
    return t;
}
#endif

template<>
std::string && fw<std::string>(std::string&& t) noexcept
{
    std::cout << "specialised r-value" << std::endl;
    return std::move(t);
}

}

Test code for:

int main()
{
    test::fw<int>(7);
    int n;
    test::fw<int>(n);
    test::fw<std::string>(std::string("alda"));
    std::string s("daal");
    test::fw<std::string>(s);
    return 0;
}

should provide output

standard
standard
specialised r-value
specialised non-const

Just one single overload for l-value references would be just as fine.

Instead compilation fails with error

<source>:27:20: error: template-id 'fw<const std::string>' for 'const std::string& test::fw(const std::string&)' does not match any template declaration
   27 | std::string const& fw<std::string const>(std::string const& t) noexcept
      |                    ^~~~~~~~~~~~~~~~~~~~~
<source>:15:5: note: candidates are: 'template<class T> T&& test::fw(typename std::remove_reference<_Tp>::type&&)'
   15 | T&& fw(typename std::remove_reference<T>::type&& t) noexcept
      |     ^~
<source>:8:5: note:                 'template<class T> T&& test::fw(typename std::remove_reference<_Tp>::type&)'
    8 | T&& fw(typename std::remove_reference<T>::type& t) noexcept
      |     ^~

Template declaration doesn't match (see godbolt as well) – obviously only for the l-value reference overload, though, but I cannot tell why, as there is a generic overload for typename std::remove_reference<T>::type&.

Provided I could specialise accordingly, then I could just re-implement std::forward within my own namespace, provide the specialisations and then use all these – leading to the final question: How can I achieve this without re-implementing std::forward from scratch? (Please follow the link).

CodePudding user response:

If you must add specializations for standard template functions before C 20, just add them into namespace std.

According to [namespace.std], only user-defined classes are allowed.

A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.

If you want to add a specialization of std::string for std::forward, it's undefined. You have to make your own forward function.

namespace test
{

// primary template for l-value argument
template<typename T>
constexpr T&&
forward(typename std::remove_reference<T>::type& t) noexcept
{
    std::cout << "standard" << std::endl;
    return static_cast<T&&>(t);
}

// specialization for T = std::string&
template <>
std::string& forward(std::string& t) noexcept
{
    std::cout << "specialised l-value" << std::endl;
    return t;
}

// specialization for T = std::string
template <>
std::string&& forward(std::string& t) noexcept
{
    std::cout << "specialised r-value" << std::endl;
    return std::move(t);
}

// primary template for r-value argument
template<typename T>
constexpr T&&
forward(typename std::remove_reference<T>::type&& t) noexcept
{
    std::cout << "standard (r-value)" << std::endl;
    static_assert
    (
            !std::is_lvalue_reference<T>::value,
            "template argument substituting T is an lvalue reference type"
    );
    return static_cast<T&&>(t);
}

// primary template for r-value argument
template <>
std::string&& forward(std::string&& t) noexcept
{
    std::cout << "specialised r-value" << std::endl;
    return std::move(t);
}

}

This code is also legal in C 20. Demo


If you want to add self-defined class specializations, do like this.

struct A {};
namespace std {
template<>
A&& forward(A& a) noexcept {
    return std::move(a);
}

template<>
A& forward(A& a) noexcept {
    return a;
}
}

Adding specializations for standard template functions is disabled since C 20. And it's not recommended to do so even before C 20. The reason is sensible, for more reading see this question

CodePudding user response:

To be noted in advance: This attempt is entirely illegal since C 20 and illegal for the given use case (replacing std::string) as well as template specialisations for standard types have been illegal even before C 20 (thanks @Nimrod for the hint and reference to the standard).

Note, too, that even for custom types (before C 20) there are better approaches to replace one specific type (see e.g. here), so this approach should not be followed anyway (I personally only attempted to complete it out of curiosity).

The actual problem (that I missed) is that in template argument deduction in case of l-value references T is decuced to a reference type. Thus for l-value references the template parameter must be turned into a reference as well:

template<>
std::string const& fw<std::string const&>(std::string const& t) noexcept;
//                                     ^ (!)
template<>
std::string& fw<std::string&>(std::string& t) noexcept;
//                         ^ (!)

Et voilà, code compiles and for the test code as presented prints the desired output provided one selects the appropriate overloads as well:

    int n = 7;
    test::fw<int&>(n);
    //          ^ (!)
    std::string s("alda");
    test::fw<std::string&>(s); // alternatively const overload
    //                  ^ (!)

An interesting fact is that these overloads apparently don't seem to work within the following demo:

template <typename ... T>
void demo(T&&... t)
{
    using test::fw;
    ( fw<T>(t), ... );
    std::cout << std::endl;
    ( fw<T>(std::forward<T>(t)), ... );
}

prints

standard l-value
standard l-value
standard l-value
specialised non-const

standard r-value
standard l-value
specialised r-value
specialised non-const

seeming to require another std::forward – but in fact, all function arguments within demo are indeed l-values and thus the standard version needs to be called. In case of r-values T is deduced to non-reference, though, and resulting return type gets T&&, i. e. the r-value reference just as desired. However the standard version getting called for r-value std::string reveals that for replacing that type entirely one overload is yet lacking:

template<>
std::string&& fw<std::string>(std::string& t) noexcept
{
    std::cout << "specialised r-value" << std::endl;
    return std::move(t);
}

While the r-value reference overload remains valid for cases where an intermediate function call returns such one (like the std::forward in second test call).

  • Related