1
votes

I'm trying to declare a function template that should accept and return a non-const reference when passed an lvalue, but return an RVO-compatible local copy when passed an rvalue:

<template ?>
? StringReplace(? str, ? from, ? to);

I want template to generate the following signatures:

  1. for non-const lvalues

    std::string& StringReplace(std::string& str, const std::string& from, const std::string& to);
    
  2. for const lvalues

    std::string StringReplace(const std::string& str, const std::string& from, const std::string& to);
    
  3. for non-const rvalues

    std::string StringReplace(std::string&&, const std::string&, const std::string&)
    
  4. for const rvalues

    std::string StringReplace(const std::string&&, const std::string&, const std::string&)
    
  5. for string literal

    std::string StringReplace(std::string&&, const std::string&, const std::string&)
    

Is it possible to specify one using single template? Perhaps there is a function or method in the standard library that achieves the same result by using one or multiple templates I should use as a reference?


See my answer for the version I ended up with.

3
Forwarding references are based on template argument type deduction. If you explicitly specify the type, it stops being a forwarding reference. There is no way to make the explicitly specified version behave differently based on the parameter since there is no deduction going on. - Nicol Bolas
Do you mean [2] (and only [2]) will not be generated? - Kentzo
@Kentzo: I mean that #2, #4, and #5 cannot all be true of the same function. - Nicol Bolas
Also, how do you plan to use the object you're given? Your function is called StringReplace; are you doing a modification of the value you're given? If so, how do you deal with being given a const&? - Nicol Bolas
@NicolBolas well, you can do T t = std::move(x); which will work even if x were declared as const reference. It falls back to copy for non-movable types - M.M

3 Answers

3
votes

I believe this basic idea meets your requirements:

template<typename Str>
Str foo(Str&& str)
{
    return std::forward<Str>(str);
}

If the argument is a non-const lvalue, then Str is deducted to S& and the forward resolves to S& also.

If the argument is an rvalue then Str is deduced to S, and the return value is copy/move-constructed from the argument.

If you explicitly give the template argument, then forwarding reference deduction is suppressed and you would have to make sure to give S& if the function argument is an lvalue that S& can bind directly to; or S otherwise.

There is never RVO for a function parameter passed by reference; e.g. suppose the calling context were std::string s = StringReplace( std::string("foo"), x, Y); , the compiler cannot know at this point to use the same memory space for s as the temporary string. The best you can do is to move-construct the return value.


Note: Your original code tries to deduce Str for all 3 arguments, this causes deduction conflicts. You should deduce the forwarding reference and for the other two, either use a non-deduced context or a different template parameter. For example:

template<typename Str, typename T>
Str StringReplace(Str&& str, T const& from, T const& to)

or use the CRef as shown in super's answer (deduction is disabled if the parameter appears to the left of ::).

2
votes

With some aggressive molding of parameters and return value, this seems to do almost what you specified.

Case 2 need to be specified with <std::string&> or the forwarding reference will not work.

#include <iostream>
#include <type_traits>

template <typename T>
using CRef = typename std::remove_reference<T>::type const&;

template<typename Str>
Str StringReplace(Str&& str, CRef<Str> from, CRef<Str> to)
{
    std::cout << __PRETTY_FUNCTION__ << std::endl;

    if (std::is_same<std::string&&, decltype(str)>::value)
        std::cout << "rvalue-ref\n\n";
    else if (std::is_same<std::string&, decltype(str)>::value)
        std::cout << "lvalue-ref\n\n";

    return std::forward<Str>(str);
}

int main() {
    std::string s1;

    StringReplace(s1, "", "");

    // Forwarding reference will deduce Str to std::string& when passing an lvalue
    StringReplace<std::string&>(s1, "", "");

    StringReplace(std::move(s1), "", "");

    StringReplace<std::string>(std::move(s1), "", "");

    StringReplace<std::string>("", "", "");

    const std::string& test = s1;
    StringReplace(test, "", "");
}

A question mark remains for how to deal with const & being passed in. As you can see if you run this, it will also return a const & as it stands now.

Live example

1
votes

Based on the commentaries and answers I ended up with two templates:

// true only if T is const
template<typename T>
using StringReplaceIsConst = std::conditional_t<std::is_const<std::remove_reference_t<T>>::value, std::true_type, std::false_type>;


// lvalue, lvalue reference, rvalue, rvalue reference, string literal
template<typename Str, typename = std::enable_if_t<!StringReplaceIsConst<Str>::value>>
Str StringReplace(
    Str&& str,
    const std::remove_reference_t<Str>& from,
    const std::remove_reference_t<Str>& to
) {
    return std::forward<Str>(str);
}

// const lvalue, const lvalue reference, const rvalue, const rvalue reference
template<typename Str, typename = std::enable_if_t<StringReplaceIsConst<Str>::value>>
std::decay_t<Str> StringReplace(
    Str&& str,
    const std::remove_reference_t<Str>& from,
    const std::remove_reference_t<Str>& to
) {
    std::decay_t<Str> mutableStr{std::forward<Str>(str)};
    StringReplace(mutableStr, from, to);
    return mutableStr;
}

Live example


While the version above works I find it impractical. The user is actually interested in whether modification is done in place or in a copy:

  • Simpler debugging: user can add logging to verify which version is called
  • Static analysis: user can annotate the definition to make compiler automatically issue warnings

With these points in mind:

// true only if T is a non-const lvalue reference
template<typename T>
using StringReplaceIsInPlace = std::conditional_t<std::is_lvalue_reference<T>::value && !std::is_const<std::remove_reference_t<T>>::value, std::true_type, std::false_type>;

// lvalue, lvalue reference, rvalue reference, 
template<typename Str, typename = std::enable_if_t<StringReplaceIsInPlace<Str>::value>>
Str StringReplace(
    Str&& str,
    const std::remove_reference_t<Str>& from,
    const std::remove_reference_t<Str>& to
) {
    return std::forward<Str>(str); // forward might be redundant, as Str is always an lvalue reference.
}

// const lvalue, const lvalue reference, rvalue, const rvalue, const rvalue reference, string literal
// std::decay ensures that return is by-value and compiler can use RVO
template<typename Str, typename = std::enable_if_t<!StringReplaceIsInPlace<Str>::value>>
std::decay_t<Str> StringReplace(
    Str&& str,
    const std::remove_reference_t<Str>& from,
    const std::remove_reference_t<Str>& to
) {
    std::decay_t<Str> mutableStr{std::forward<Str>(str)}; // move construct for non-const rvalues, otherwise copy-construct
    StringReplace(mutableStr, from, to);
    return mutableStr; // RVO-compatible
}

The second declaration can be annotated with [[nodiscard]] (C++17), [[gnu::warn_unused_result]] (clang and gcc) or _Check_return_ (msvs).

Live example