There is no engineering like over engineering.
In this case, I create a type string_builder::op<?>
that reasonably efficiently collects a pile of strings to concatenate, and when cast into a std::string
proceeds to do so.
It stores copies of any temporary std::string
s provided, and references to longer-lived ones, as a bit of paranoia.
It ends up reducing to:
std::string retval;
retval.reserve(the right amount);
retval+=perfect forwarded first string
...
retval+=perfect forwarded last string
return retval;
but it wraps it all in lots of syntaxtic sugar.
namespace string_builder {
template<class String, class=std::enable_if_t< std::is_same< String, std::string >::value >>
std::size_t get_size( String const& s ) { return s.size(); }
template<std::size_t N>
constexpr std::size_t get_size( const char(&)[N] ) { return N; }
template<std::size_t N>
constexpr std::size_t get_size( char(&)[N] ) { return N; }
std::size_t get_size( const char* s ) { return std::strlen(s); }
template<class Indexes, class...Ss>
struct op;
struct tuple_tag {};
template<size_t... Is, class... Ss>
struct op<std::integer_sequence<size_t, Is...>, Ss...> {
op() = default;
op(op const&) = delete;
op(op&&) = default;
std::tuple<Ss...> data;
template<class... Tuples>
op( tuple_tag, Tuples&&... ts ): data( std::tuple_cat( std::forward<Tuples>(ts)... ) ) {}
std::size_t size() const {
std::size_t retval = 0;
int unused[] = {((retval+=get_size(std::get<Is>(data))), 0)..., 0};
(void)unused;
return retval;
}
operator std::string() && {
std::string retval;
retval.reserve( size()+1 );
int unused[] = {((retval+=std::forward<Ss>(std::get<Is>(data))), 0)..., 0};
(void)unused;
return retval;
}
template<class S0>
op<std::integer_sequence<size_t, Is..., sizeof...(Is)>, Ss..., S0>
operator+(S0&&s0)&& {
return { tuple_tag{}, std::move(data), std::forward_as_tuple( std::forward<S0>(s0) ) };
}
auto operator()()&& {return std::move(*this);}
template<class T0, class...Ts>
auto operator()(T0&&t0, Ts&&... ts)&&{
return (std::move(*this)+std::forward<T0>(t0))(std::forward<Ts>(ts)...);
}
};
}
string_builder::op< std::integer_sequence<std::size_t> >
string_build() { return {}; }
template<class... Strings>
auto
string_build(Strings&&...strings) {
return string_build()(std::forward<Strings>(strings)...);
}
and now we get:
std::string Concatenate(const std::string& s1,
const std::string& s2,
const std::string& s3,
const std::string& s4,
const std::string& s5)
{
return string_build() + s1 + s2 + s3 + s4 + s5;
}
or more generically and efficiently:
template<class... Strings>
std::string Concatenate(Strings&&...strings)
{
return string_build(std::forward<Strings>(strings)...);
}
there are extraneous moves, but no extraneous allocations. And it works with raw "strings"
with no extra allocations.
live example