std::string Concatenate(const std::string& s1,
const std::string& s2,
const std::string& s3,
const std::string& s4,
const std::string& s5)
{
return s1 + s2 + s3 + s4 + s5;
}
默认情况下,return s1 + s2 + s3 + s4 + s5;
可能等同于以下代码:
auto t1 = s1 + s2; // Allocation 1
auto t2 = t1 + s3; // Allocation 2
auto t3 = t2 + s4; // Allocation 3
return t3 + s5; // Allocation 4
有一种优雅的方法可以将分配时间减少到1吗?我的意思是保持return s1 + s2 + s3 + s4 + s5;
不变,但效率会自动提高。如果可能,它还可以避免程序员滥用std::string::operator +
。
ref-qualifier 成员函数有帮助吗?
答案 0 :(得分:12)
问题的前提是:
s1 + s2 + s3 + s4 + s5 + ... + sn
将要求n分配不正确。
相反,它将需要O(Log(n))分配。第一个s1 + s1
将生成一个临时的。随后,临时(rvalue)将成为所有后续+
操作的左参数。该标准规定,当string +
的lhs为rvalue时,该实现只是附加到该临时值并将其移出:
operator+(basic_string<charT,traits,Allocator>&& lhs,
const basic_string<charT,traits,Allocator>& rhs);
Returns: std::move(lhs.append(rhs))
该标准还规定了字符串的容量将以几何方式增长(1.5和2之间的因子是常见的)。因此,在每次分配时,容量将以几何方式增长,并且该容量沿着+
操作链传播。更具体地说,原始代码:
s = s1 + s2 + s3 + s4 + s5 + ... + sn;
实际等同于:
s = s1 + s2;
s += s3;
s += s4;
s += s5;
// ...
s += sn;
当几何容量增长与短串优化相结合时,“预先保留”正确容量的值是有限的。如果这样的代码实际上显示为性能测试的热点,我只会费心去做。
答案 1 :(得分:7)
std::string combined;
combined.reserve(s1.size() + s2.size() + s3.size() + s4.size() + s5.size());
combined += s1;
combined += s2;
combined += s3;
combined += s4;
combined += s5;
return combined;
答案 2 :(得分:4)
没有像过度工程这样的工程。
在这种情况下,我创建了一个类型string_builder::op<?>
,它可以合理有效地收集一堆字符串以进行连接,并在转换为std::string
时继续这样做。
它存储所提供的任何临时std::string
的副本,以及对较长寿的副本的引用,作为一点偏执。
最终减少到:
std::string retval;
retval.reserve(the right amount);
retval+=perfect forwarded first string
...
retval+=perfect forwarded last string
return retval;
但它包含了大量的合成糖。
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)...);
}
现在我们得到:
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;
}
或更具普遍性和效率:
template<class... Strings>
std::string Concatenate(Strings&&...strings)
{
return string_build(std::forward<Strings>(strings)...);
}
有无关的动作,但没有多余的分配。它适用于原始"strings"
而没有额外的分配。
答案 3 :(得分:1)
您可以使用以下代码:
std::string(s1) + s2 + s3 + s4 + s5 + s6 + ....
这将分配一个未命名的临时(第一个字符串的副本),然后将每个其他字符串附加到它。智能优化器可以将其优化为与其他人发布的保留+附加代码相同的代码,因为所有这些功能通常都是可以内联的。
这可以通过使用operator +的移动增强版本来实现,其定义为(粗略地)
std::string operator+(std::string &&lhs, const std::string &rhs) {
return std::move(lhs.append(rhs));
}
结合RVO,意味着不需要创建或销毁其他string
个对象。
答案 4 :(得分:0)
经过一番思考,我认为至少考虑一种稍微不同的方法可能是值得的。
std::stringstream s;
s << s1 << s2 << s3 << s4 << s5;
return s.str();
虽然它并不能保证只有一次分配,但我们可以期望优化stringstream
来累积相对大量的数据,所以很有可能(除非输入字符串很大)它会使分配数量保持在最低水平。
同时,特别是如果单个字符串相当小,它肯定避免了我们期望的情况,例如a + b + c + d
,其中(至少在C ++ 03中)我们期望在评估表达式的过程中查看创建和销毁的一些临时对象。实际上,我们通常可以预期这会得到与表达模板之类的结果相同的结果,但复杂性要低得多。
虽然存在一些不利因素:iostream(一般情况下)有足够的行李,例如关联的语言环境,特别是如果字符串很小,创建流的开销可能比我们保存在单独的分配中要多。
使用当前的编译器/库,我预计创建流的开销会使速度变慢。对于较旧的实现,我必须进行测试以确定是否具有任何确定性(并且我没有足够的编译器方便这样做)。
答案 5 :(得分:0)
这个怎么样:
std::string Concatenate(const std::string& s1,
const std::string& s2,
const std::string& s3,
const std::string& s4,
const std::string& s5)
{
std::string ret;
ret.reserve(s1.length() + s2.length() + s3.length() + s4.length() + s5.length());
ret.append(s1.c_str());
ret.append(s2.c_str());
ret.append(s3.c_str());
ret.append(s4.c_str());
ret.append(s5.c_str());
return ret;
}
有两个分配,一个非常小,可以为数据构建std::string
另一个预留内存。