在使用表达式模板时,根据左值/右值提供类的不同实现
Providing different implementations of a class depending on lvalue/rvalue when using expression templates
问题
假设我们实现了一个string
类,它表示字符串。然后,我们希望添加一个连接两个string
的operator+
,并决定通过表达式模板实现,以避免在执行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使用了它
auto result = str1 + str2 + ... + strN; // guess what's going on here?
问题
底线是:一旦试图存储表达式模板本身,这种实现表达式模板的方式(通过存储廉价引用,而不是复制值或使用共享指针)似乎就会被破坏。
因此,我非常喜欢检测我是在构建右值还是左值的方法,并且根据是构建右值(保留引用)还是构建左值(制作副本)提供表达式模板的不同实现。
是否有现成的设计模式来处理这种情况
在我的研究中,我唯一能弄清楚的是
根据
this
是左值或右值,即,可以重载成员函数class C { void f() &; void f() &&; // called on temporaries }
然而,我似乎也不能在构造函数上做到这一点。
在C++中,不能真正执行"类型重载",即提供同一类型的多个实现,这取决于如何使用该类型(创建为左值或右值的实例)。
我在一个注释中开始了这个,但它有点大。然后,让我们把它作为一个答案(即使它并不能真正回答你的问题)。
这是auto
的已知问题。例如,Herb Sutter在这里讨论过它,Motti Lanzkron在这里更详细地讨论过它。
正如他们所说,委员会曾讨论将operator auto
添加到C++中以解决这个问题。这个想法将取代(或除了)提供
operator string() const;
正如你所提到的,我们将提供
string operator auto() const;
用于类型推导上下文。在这种情况下,
auto result = str1 + str2 + ... + strN;
不会推断出result
的类型是"正确的类型",而是string
的类型,因为这是operator auto()
返回的。
AFAICT这在C++14中是不会发生的。C++17可能。。。
详述我对OP的评论;示例:
这只解决了分配给对象或绑定到引用,然后转换为目标类型的问题。这并不是一个全面的解决问题的方法(也可以参见Yakk对我的评论的回应),但它阻止了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{};
}
只是一个疯狂的想法(还没有尝试过):
template<class T, class U>
class stringbuilder
{
stringbuilder(stringbuilder const &) = delete;
}
不会强制编译错误吗?
一种可能的方法是使用null对象模式。虽然它可能会使字符串生成器更大,但它仍然可以避免内存分配。
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) {}
//...
如果构造函数的参数是左值,则存储对实际对象的引用。如果构造函数的参数是一个右值,您可以将其移动到一个内部变量中,而几乎不需要任何成本(移动操作很便宜),并存储对该内部对象的引用。代码的其余部分可以访问引用,知道(好吧,至少希望)字符串仍然有效。
hopping部分是因为如果传递了一个左值,但在字符串生成器完成其工作之前对象就被破坏了,那么就没有什么可以阻止误用的。
这里是解决悬挂引用问题的另一种尝试。不过,它并不能解决引用已修改内容的问题。
这个想法是将临时值存储到值中,但要有对lvalue的引用(我们可以期望在;
之后继续生存)。
// 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 << "RVREFn";
auto r = c + f() + "toto";
std::cout << r << "n";
std::string s (r);
std::cout << s << "n";
}
{
std::cout << "nnLVREFn";
std::string str="lvref";
auto r = c + str + "toto";
std::cout << r << "n";
std::string s (r);
std::cout << s << "n";
}
{
std::cout << "nnCLVREFn";
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
,它们不是困难所在。
附言:我使用C++14只是为了简化类型特征。
- 在C++中实现正则表达式
- 实现基于数字值(正、负、零)的条件表达式的最佳方法
- 计算简单数学表达式的函数的实现.(C++)
- 如何在不使用 lambda 表达式的情况下实现特定的比较器
- 实现 vector 时二进制表达式的操作数无效
- 我可以使用折叠表达式实现Max(A Max(B,Max(C,D)))
- 为什么 C++11 正则表达式(libc ++ 实现)如此缓慢?
- 如何实现正则表达式
- 如何修复 pimpl 实现中预期的主要表达式编译错误
- 表达式模板实现未优化
- 我应该如何使用表达式模板来实现数学向量类的标量乘法
- 在使用表达式模板时,根据左值/右值提供类的不同实现
- Microsoft 的 std::正则表达式的实现
- 在实现特定的神经网络时,如何获得梯度表达式
- 实现 Matlab 的冒号:表达式模板类中的运算符C++
- 判断表达式的堆栈实现
- 在C++表达式模板编程中是否可能实现高效的"repeatedly used intermediates"?
- 可视化 如何在没有正则表达式的情况下实现C++高效的全词字符串替换
- Perl正则表达式运行速度快于c++ Boost实现
- 字符串实现和表达式模板