使用对临时对象的引用的表达式模板是否可以重复使用?
Can expression templates using references to temporaries be re-useable?
我正在努力思考表达式模板。在 wikipedia article 中给出了一个示例,其中表达式模板 VecSum
将 const 引用 存储到它的两个操作数。 Vec
是一个包含 std::vector<double>
的表达式模板。我将首先提出我的问题,然后给出下面示例的完整概要。
我可以重复使用对临时变量使用 const 引用的表达式吗?如果没有,我将如何实现轻量级、可重复使用的表达式模板?
对于三个 Vec
s a
、b
和 c
,表达式 a+b+c
的类型为
VecSum<VecSum<Vec, Vec>, Vec>
如果我理解正确的话,内部 VecSum
是临时的,外部 VecSum
存储对内部 VecSum
的 const 引用。我相信内部 VecSum
临时的生命周期得到保证 直到 表达式 a+b+c
被评估。正确的?这是否意味着在没有创建悬空引用的危险的情况下不能重用表达式?
auto expr = a + b + c;
Vec v1 = expr; // ok
Vec v2 = expr; // not ok!
如果可以,如何修改这个例子,使
- 表达式可重用
- 表达式不存储其操作数的副本(至少在不需要的情况下)?
完整代码示例
为了完整性 - 以防维基百科文章同时更新,让我在这里重复示例代码并在 main
中给出一个我认为会创建悬空引用的示例。
#include <cassert>
#include <vector>
template <typename E>
class VecExpression {
public:
double operator[](size_t i) const
{
// Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
return static_cast<E const&>(*this)[i];
}
size_t size() const { return static_cast<E const&>(*this).size(); }
};
class Vec : public VecExpression<Vec> {
std::vector<double> elems;
public:
double operator[](size_t i) const { return elems[i]; }
double &operator[](size_t i) { return elems[i]; }
size_t size() const { return elems.size(); }
Vec(size_t n) : elems(n) {}
// construct vector using initializer list
Vec(std::initializer_list<double> init) : elems(init) {}
// A Vec can be constructed from any VecExpression, forcing its evaluation.
template <typename E>
Vec(VecExpression<E> const& expr) : elems(expr.size()) {
for (size_t i = 0; i != expr.size(); ++i) {
elems[i] = expr[i];
}
}
};
template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2> > {
E1 const& _u;
E2 const& _v;
public:
VecSum(E1 const& u, E2 const& v) : _u(u), _v(v) {
assert(u.size() == v.size());
}
double operator[](size_t i) const { return _u[i] + _v[i]; }
size_t size() const { return _v.size(); }
};
template <typename E1, typename E2>
VecSum<E1, E2>
operator+(VecExpression<E1> const& u, VecExpression<E2> const& v) {
return VecSum<E1, E2>(*static_cast<const E1*>(&u), *static_cast<const E2*>(&v));
}
int main() {
Vec v0 = {23.4,12.5,144.56,90.56};
Vec v1 = {67.12,34.8,90.34,89.30};
Vec v2 = {34.90,111.9,45.12,90.5};
auto expr = v0 + v1 + v2;
Vec v1 = expr; // ok
Vec v2 = expr; // not ok!
}
编辑:
我刚刚意识到这可能是 的副本。然而,这两个问题的答案却大不相同,而且都很有用。
上面的评论有一个非常有效的方法来检查悬空引用的问题。请注意,如果您尝试在您的示例中打印来自 main 函数的值,该程序仍然可以运行,因为将绑定悬空引用的对象也会在 main 的堆栈 space 上创建。我试图将分配给 expr 的代码移动到一个函数中,程序按预期崩溃了(临时对象将在另一个堆栈框架中):
auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
return v0 + v1 + v2;
}
// ... in main:
auto expr = makeExpr1(v0, v1, v2);
您在此处强调的问题出现在创建表达式的情况下,该表达式可以在 C++ 等语言中延迟计算。在范围表达式(C++20 范围)的上下文中可能会出现某种类似的情况。
下面是我快速尝试修复该代码并使其与添加了运算符 + 的左值和右值一起工作(对于丑陋的部分和可能的错误,我深表歉意)。这将仅在它们将超出范围时存储它们的操作数的副本,并且会导致旧代码中的悬空引用。
关于re-usability:只要为每个操作定义一个类型和对应的运算符'?'即可函数('?' 是操作的符号)这个方法应该为您提供对此类向量进行任何二进制操作的起点。
#include <cassert>
#include <vector>
#include <utility>
#include <iostream>
/*
* Passes lvalues and stores rvalues
*/
template <typename T> class Wrapper;
template <typename T> class Wrapper<T&> {
private:
T& ref;
public:
Wrapper(T& ref) : ref(ref) {}
T& get() { return ref; }
const T& get() const { return ref; }
};
template <typename T> class Wrapper<T&&> {
private:
T value;
public:
Wrapper(T&& ref) : value(std::move(ref)) {}
T& get() { return value; }
const T& get() const { return value; }
};
template <typename E>
class VecExpression {
public:
double operator[](size_t i) const
{
// Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
return static_cast<E const&>(*this)[i];
}
size_t size() const { return static_cast<E const&>(*this).size(); }
};
/*
* Forwards the reference and const qualifiers
* of the expression type to the expression itself
*/
template <typename E> constexpr E& forwardRef(VecExpression<E>& ve) {
return static_cast<E&>(ve);
}
template <typename E> constexpr const E& forwardRef(const VecExpression<E>& ve) {
return static_cast<const E&>(ve);
}
template <typename E> constexpr E&& forwardRef(VecExpression<E>&& ve) {
return static_cast<E&&>(ve);
}
class Vec : public VecExpression<Vec> {
std::vector<double> elems;
public:
double operator[](size_t i) const { return elems[i]; }
double &operator[](size_t i) { return elems[i]; }
size_t size() const { return elems.size(); }
Vec(size_t n) : elems(n) {}
// construct vector using initializer list
Vec(std::initializer_list<double> init) : elems(init) {}
// A Vec can be constructed from any VecExpression, forcing its evaluation.
template <typename E>
Vec(VecExpression<E> const& expr) : elems(expr.size()) {
std::cout << "Expr ctor\n"; // Very quick test
for (size_t i = 0; i != expr.size(); ++i) {
elems[i] = expr[i];
}
}
// Move ctor added for checking
Vec(Vec&& vec) : elems(std::move(vec.elems)) {
std::cout << "Move ctor\n"; // Very quick test
}
};
/*
* Now VecSum is a sum between possibly const - qualified
* and referenced expression types
*/
template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2>> {
Wrapper<E1> _u;
Wrapper<E2> _v;
public:
VecSum(E1 u, E2 v) : _u(static_cast<E1>(u)), _v(static_cast<E2>(v)) {
assert(_u.get().size() == _v.get().size());
}
double operator[](size_t i) const { return _u.get()[i] + _v.get()[i]; }
size_t size() const { return _v.get().size(); }
};
/*
* Used to create a VecSum by capturing also the reference kind
* of the arguments (will be used by the Wrapper inside VecSum)
*/
template <typename E1, typename E2>
auto makeVecSum(E1&& e1, E2&& e2) {
return VecSum<E1&&, E2&&>(std::forward<E1>(e1), std::forward<E2>(e2));
}
/*
* Now the operator+ takes the vector expressions by universal references
*/
template <typename VE1, typename VE2>
auto operator+(VE1&& ve1, VE2&& ve2) {
return makeVecSum(forwardRef(std::forward<VE1>(ve1)), forwardRef(std::forward<VE2>(ve2)));
}
// Now this will work
auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
return v0 + v1 + v2;
}
// This will also work - the rvalue is stored in the
// expression itself and both will have the same lifetime
auto makeExpr2(Vec const& v0, Vec const& v1) {
return v0 + v1 + Vec({1.0, 1.0, 1.0, 1.0});
}
int main() {
Vec v0 = {23.4,12.5,144.56,90.56};
Vec v1 = {67.12,34.8,90.34,89.30};
Vec v2 = {34.90,111.9,45.12,90.5};
auto expr = makeExpr1(v0, v1, v2);
Vec v1_ = expr;
Vec v2_ = expr;
auto expr_ = makeExpr2(v0, v1);
for (size_t i = 0; i < v1_.size(); ++i)
std::cout << v1_[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < v2_.size(); ++i)
std::cout << v2_[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < expr.size(); ++i)
std::cout << expr[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < expr_.size(); ++i)
std::cout << expr_[i] << " ";
std::cout << std::endl;
}
我正在努力思考表达式模板。在 wikipedia article 中给出了一个示例,其中表达式模板 VecSum
将 const 引用 存储到它的两个操作数。 Vec
是一个包含 std::vector<double>
的表达式模板。我将首先提出我的问题,然后给出下面示例的完整概要。
我可以重复使用对临时变量使用 const 引用的表达式吗?如果没有,我将如何实现轻量级、可重复使用的表达式模板?
对于三个 Vec
s a
、b
和 c
,表达式 a+b+c
的类型为
VecSum<VecSum<Vec, Vec>, Vec>
如果我理解正确的话,内部 VecSum
是临时的,外部 VecSum
存储对内部 VecSum
的 const 引用。我相信内部 VecSum
临时的生命周期得到保证 直到 表达式 a+b+c
被评估。正确的?这是否意味着在没有创建悬空引用的危险的情况下不能重用表达式?
auto expr = a + b + c;
Vec v1 = expr; // ok
Vec v2 = expr; // not ok!
如果可以,如何修改这个例子,使
- 表达式可重用
- 表达式不存储其操作数的副本(至少在不需要的情况下)?
完整代码示例
为了完整性 - 以防维基百科文章同时更新,让我在这里重复示例代码并在 main
中给出一个我认为会创建悬空引用的示例。
#include <cassert>
#include <vector>
template <typename E>
class VecExpression {
public:
double operator[](size_t i) const
{
// Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
return static_cast<E const&>(*this)[i];
}
size_t size() const { return static_cast<E const&>(*this).size(); }
};
class Vec : public VecExpression<Vec> {
std::vector<double> elems;
public:
double operator[](size_t i) const { return elems[i]; }
double &operator[](size_t i) { return elems[i]; }
size_t size() const { return elems.size(); }
Vec(size_t n) : elems(n) {}
// construct vector using initializer list
Vec(std::initializer_list<double> init) : elems(init) {}
// A Vec can be constructed from any VecExpression, forcing its evaluation.
template <typename E>
Vec(VecExpression<E> const& expr) : elems(expr.size()) {
for (size_t i = 0; i != expr.size(); ++i) {
elems[i] = expr[i];
}
}
};
template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2> > {
E1 const& _u;
E2 const& _v;
public:
VecSum(E1 const& u, E2 const& v) : _u(u), _v(v) {
assert(u.size() == v.size());
}
double operator[](size_t i) const { return _u[i] + _v[i]; }
size_t size() const { return _v.size(); }
};
template <typename E1, typename E2>
VecSum<E1, E2>
operator+(VecExpression<E1> const& u, VecExpression<E2> const& v) {
return VecSum<E1, E2>(*static_cast<const E1*>(&u), *static_cast<const E2*>(&v));
}
int main() {
Vec v0 = {23.4,12.5,144.56,90.56};
Vec v1 = {67.12,34.8,90.34,89.30};
Vec v2 = {34.90,111.9,45.12,90.5};
auto expr = v0 + v1 + v2;
Vec v1 = expr; // ok
Vec v2 = expr; // not ok!
}
编辑:
我刚刚意识到这可能是
上面的评论有一个非常有效的方法来检查悬空引用的问题。请注意,如果您尝试在您的示例中打印来自 main 函数的值,该程序仍然可以运行,因为将绑定悬空引用的对象也会在 main 的堆栈 space 上创建。我试图将分配给 expr 的代码移动到一个函数中,程序按预期崩溃了(临时对象将在另一个堆栈框架中):
auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
return v0 + v1 + v2;
}
// ... in main:
auto expr = makeExpr1(v0, v1, v2);
您在此处强调的问题出现在创建表达式的情况下,该表达式可以在 C++ 等语言中延迟计算。在范围表达式(C++20 范围)的上下文中可能会出现某种类似的情况。 下面是我快速尝试修复该代码并使其与添加了运算符 + 的左值和右值一起工作(对于丑陋的部分和可能的错误,我深表歉意)。这将仅在它们将超出范围时存储它们的操作数的副本,并且会导致旧代码中的悬空引用。
关于re-usability:只要为每个操作定义一个类型和对应的运算符'?'即可函数('?' 是操作的符号)这个方法应该为您提供对此类向量进行任何二进制操作的起点。
#include <cassert>
#include <vector>
#include <utility>
#include <iostream>
/*
* Passes lvalues and stores rvalues
*/
template <typename T> class Wrapper;
template <typename T> class Wrapper<T&> {
private:
T& ref;
public:
Wrapper(T& ref) : ref(ref) {}
T& get() { return ref; }
const T& get() const { return ref; }
};
template <typename T> class Wrapper<T&&> {
private:
T value;
public:
Wrapper(T&& ref) : value(std::move(ref)) {}
T& get() { return value; }
const T& get() const { return value; }
};
template <typename E>
class VecExpression {
public:
double operator[](size_t i) const
{
// Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
return static_cast<E const&>(*this)[i];
}
size_t size() const { return static_cast<E const&>(*this).size(); }
};
/*
* Forwards the reference and const qualifiers
* of the expression type to the expression itself
*/
template <typename E> constexpr E& forwardRef(VecExpression<E>& ve) {
return static_cast<E&>(ve);
}
template <typename E> constexpr const E& forwardRef(const VecExpression<E>& ve) {
return static_cast<const E&>(ve);
}
template <typename E> constexpr E&& forwardRef(VecExpression<E>&& ve) {
return static_cast<E&&>(ve);
}
class Vec : public VecExpression<Vec> {
std::vector<double> elems;
public:
double operator[](size_t i) const { return elems[i]; }
double &operator[](size_t i) { return elems[i]; }
size_t size() const { return elems.size(); }
Vec(size_t n) : elems(n) {}
// construct vector using initializer list
Vec(std::initializer_list<double> init) : elems(init) {}
// A Vec can be constructed from any VecExpression, forcing its evaluation.
template <typename E>
Vec(VecExpression<E> const& expr) : elems(expr.size()) {
std::cout << "Expr ctor\n"; // Very quick test
for (size_t i = 0; i != expr.size(); ++i) {
elems[i] = expr[i];
}
}
// Move ctor added for checking
Vec(Vec&& vec) : elems(std::move(vec.elems)) {
std::cout << "Move ctor\n"; // Very quick test
}
};
/*
* Now VecSum is a sum between possibly const - qualified
* and referenced expression types
*/
template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2>> {
Wrapper<E1> _u;
Wrapper<E2> _v;
public:
VecSum(E1 u, E2 v) : _u(static_cast<E1>(u)), _v(static_cast<E2>(v)) {
assert(_u.get().size() == _v.get().size());
}
double operator[](size_t i) const { return _u.get()[i] + _v.get()[i]; }
size_t size() const { return _v.get().size(); }
};
/*
* Used to create a VecSum by capturing also the reference kind
* of the arguments (will be used by the Wrapper inside VecSum)
*/
template <typename E1, typename E2>
auto makeVecSum(E1&& e1, E2&& e2) {
return VecSum<E1&&, E2&&>(std::forward<E1>(e1), std::forward<E2>(e2));
}
/*
* Now the operator+ takes the vector expressions by universal references
*/
template <typename VE1, typename VE2>
auto operator+(VE1&& ve1, VE2&& ve2) {
return makeVecSum(forwardRef(std::forward<VE1>(ve1)), forwardRef(std::forward<VE2>(ve2)));
}
// Now this will work
auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
return v0 + v1 + v2;
}
// This will also work - the rvalue is stored in the
// expression itself and both will have the same lifetime
auto makeExpr2(Vec const& v0, Vec const& v1) {
return v0 + v1 + Vec({1.0, 1.0, 1.0, 1.0});
}
int main() {
Vec v0 = {23.4,12.5,144.56,90.56};
Vec v1 = {67.12,34.8,90.34,89.30};
Vec v2 = {34.90,111.9,45.12,90.5};
auto expr = makeExpr1(v0, v1, v2);
Vec v1_ = expr;
Vec v2_ = expr;
auto expr_ = makeExpr2(v0, v1);
for (size_t i = 0; i < v1_.size(); ++i)
std::cout << v1_[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < v2_.size(); ++i)
std::cout << v2_[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < expr.size(); ++i)
std::cout << expr[i] << " ";
std::cout << std::endl;
for (size_t i = 0; i < expr_.size(); ++i)
std::cout << expr_[i] << " ";
std::cout << std::endl;
}