假设我们实现了一个代表字符串的string
类。然后,我们想要添加一个operator+
来连接两个string
,并决定通过表达式模板实现该功能,以避免在执行str1 + str2 + ... + strN
时进行多次分配。
运营商将如下所示:
stringbuilder<string, string> operator+(const string &a, const string &b)
stringbuilder
是一个模板类,它反过来重载operator+
并具有隐式string
转换运算符。几乎是标准的教科书练习:
template<class T, class U> class stringbuilder;
template<> class stringbuilder<string, string> {
stringbuilder(const string &a, const string &b) : a(a), b(b) {};
const string &a;
const string &b;
operator string() const;
// ...
}
// recursive case similar,
// building a stringbuilder<stringbuilder<...>, string>
只要有人
,上述实施就完美无缺string result = str1 + str2 + ... + strN;
然而,它有一个微妙的错误。将结果分配给 right 类型的变量将使该变量保持对组成表达式的所有字符串的引用。这意味着,例如,更改其中一个字符串将改变结果:
void print(string);
string str1 = "foo";
string str2 = "bar";
right_type result = str1 + str2;
str1 = "fie";
print(result);
这将打印 fiebar ,因为存储在表达式模板中的str1引用。情况变得更糟:
string f();
right_type result = str1 + f();
print(result); // kaboom
现在,表达式模板将包含对已销毁值的引用,立即崩溃您的程序。
现在right_type
是什么?它当然是stringbuilder<stringbuilder<...>, string>
,即表达模板魔术为我们生成的类型。
现在为什么会使用这样的隐藏类型?事实上,人们并没有明确地使用它 - 但是C ++ 11的自动功能呢!
auto result = str1 + str2 + ... + strN; // guess what's going on here?
底线是:似乎这种实现表达式模板的方式(通过存储廉价的引用而不是复制值或使用共享指针)一旦尝试存储就会被破坏表达式模板本身。
因此,我非常喜欢检测我是否正在构建rvalue或左值的方法,并且提供表达式模板的不同实现,具体取决于是否构建了右值(保持引用)或构建了左值(制作副本)。
是否有一个已建立的设计模式来处理这种情况?
我在研究过程中唯一能够弄清楚的是
可以根据this
作为左值或右值来重载成员函数,即
class C {
void f() &;
void f() &&; // called on temporaries
}
然而,似乎我也不能在构造函数上做到这一点。 在C ++中,人们不能真正做``type overloads'',即提供相同类型的多个实现,具体取决于类型将如何使用(创建为左值或右值的实例)。
答案 0 :(得分:13)
我在评论中开始这个,但它有点大。那么,让我们回答一下(尽管它并没有真正回答你的问题)。
这是auto
的已知问题。例如,Herb Sutter here和Motti Lanzkron here更详细地讨论过它。
正如他们所说,委员会讨论了将operator auto
添加到C ++来解决这个问题。这个想法将取代(或除此之外)提供
operator string() const;
如你所说,人们会提供
string operator auto() const;
用于类型推导上下文。在这种情况下,
auto result = str1 + str2 + ... + strN;
不会将result
的类型推断为“正确的类型”,而是推断类型string
,因为这是operator auto()
返回的内容。
AFAICT这不会发生在C ++ 14中。 C ++ 17 pehaps ......
答案 1 :(得分:2)
阐述comment I made to the OP;例如:
这只解决了分配对象或绑定到引用以及之后转换为目标类型的问题。这不是一个全面解决问题的方法(也见Yakk的response to my comment),但它可以防止OP中出现的情况,并且通常更难编写这种容易出错的代码。
编辑:可能无法为类模板扩展此方法(更具体地说,std::move
的特化)。 Macro'ing可以解决这个特定问题,但显然很难看。重载std::move
将依赖于UB。
#include <utility>
#include <cassert>
// your stringbuilder class
struct wup
{
// only use member functions with rvalue-ref-qualifier
// this way, no lvalues of this class can be used
operator int() &&
{
return 42;
}
};
// specialize `std::move` to "prevent" from converting lvalues to rvalue refs
// (make it much harder and more explicit)
namespace std
{
template<> wup&& move(wup&) noexcept
{
assert(false && "Do not use `auto` with this expression!");
}
// alternatively: no function body -> linker error
}
int main()
{
auto obj = wup{};
auto& lref = obj;
auto const& clref = wup{};
auto&& rref = wup{};
// fail because of conversion operator
int iObj = obj;
int iLref = lref;
int iClref = clref;
int iRref = rref;
int iClref_mv = std::move(clref);
// assert because of move specialization
int iObj_mv = std::move(obj);
int iLref_mv = std::move(lref);
int iRref_mv = std::move(rref);
// works
int i = wup{};
}
答案 2 :(得分:0)
只是一个疯狂的想法(还没试过):
template<class T, class U>
class stringbuilder
{
stringbuilder(stringbuilder const &) = delete;
}
不会强制编译错误?
答案 3 :(得分:0)
可能的方法是使用空对象模式。虽然它可能会使您的字符串构建器更大,但它仍然会避免内存分配。
template <>
class stringbuilder<std::string,std::string> {
std::string lhs_value;
std::string rhs_value;
const std::string& lhs;
const std::string& rhs;
stringbuilder(const std::string &lhs, const std::string &rhs)
: lhs(lhs), rhs(rhs) {}
stringbuilder(std::string&& lhs, const std::string &rhs)
: lhs_value(std::move(lhs)), lhs(lhs_value), rhs(rhs) {}
stringbuilder(const std::string& lhs, std::string&& rhs)
: rhs_value(std::move(rhs)), lhs(lhs), rhs(rhs_value) {}
stringbuilder(std::string&& lhs, std::string&& rhs)
: lhs_value(std::move(lhs)), rhs_value(std::move(rhs)),
lhs(lhs_value), rhs(rhs_value) {}
//...
如果构造函数的参数是左值,则存储对真实对象的引用。如果构造函数的参数是rvalue,则可以将其移动到几乎没有成本的内部变量(移动操作很便宜)并存储对该内部对象的引用。其余代码可以访问引用知道(好吧,至少希望)字符串仍然存活。
希望部分是因为如果传递左值但是在stringbuilder完成其作业之前销毁了对象,则没有任何阻塞误用。
答案 4 :(得分:0)
这是解决悬挂引用问题的另一种尝试。它没有解决引用被修改的东西的问题。
这个想法是将临时值存储到值中,但要引用左值(我们可以期望在;
之后继续生活)。
// Temporary => store a copy
// Otherwise, store a reference
template <typename T>
using URefUnlessTemporary_t
= std::conditional_t<std::is_rvalue_reference<T&&>::value
, std::decay_t<T>
, T&&>
;
template <typename LHS, typename RHS>
struct StringExpression
{
StringExpression(StringExpression const&) = delete;
StringExpression(StringExpression &&) = default;
constexpr StringExpression(LHS && lhs_, RHS && rhs_)
: lhs(std::forward<LHS>(lhs_))
, rhs(std::forward<RHS>(rhs_))
{ }
explicit operator std::string() const
{
auto const len = size(*this);
std::string res;
res.reserve(len);
append(res, *this);
return res;
}
friend constexpr std::size_t size(StringExpression const& se)
{
return size(se.lhs) + size(se.rhs);
}
friend void append(std::string & s, StringExpression const& se)
{
append(s, se.lhs);
append(s, se.rhs);
}
friend std::ostream & operator<<(std::ostream & os, const StringExpression & se)
{ return os << se.lhs << se.rhs; }
private:
URefUnlessTemporary_t<LHS> lhs;
URefUnlessTemporary_t<RHS> rhs;
};
template <typename LHS, typename RHS>
StringExpression<LHS&&,RHS&&> operator+(LHS && lhs, RHS && rhs)
{
return StringExpression<LHS&&,RHS&&>{std::forward<LHS>(lhs), std::forward<RHS>(rhs) };
}
我毫不怀疑这可以简化。
int main ()
{
constexpr static auto c = exp::concatenator{};
{
std::cout << "RVREF\n";
auto r = c + f() + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
{
std::cout << "\n\nLVREF\n";
std::string str="lvref";
auto r = c + str + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
{
std::cout << "\n\nCLVREF\n";
std::string const str="clvref";
auto r = c + str + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
}
注意:我不提供size()
,append()
或concatenator
,它们不是困难所在的点。
PS:我只使用C ++ 14来简化类型特征。