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
StringReplace
; are you doing a modification of the value you're given? If so, how do you deal with being given aconst&
? - Nicol BolasT t = std::move(x);
which will work even ifx
were declared as const reference. It falls back to copy for non-movable types - M.M