【问题标题】:Avoiding need for #define with expression templates使用表达式模板避免#define
【发布时间】:2011-05-03 03:03:16
【问题描述】:

使用以下代码,“hello2”不显示,因为在第 3 行创建的临时字符串在第 4 行执行之前死亡。在第 1 行使用 #define 可以避免这个问题,但是有没有办法在不使用 #define 的情况下避免这个问题? (C++11代码没问题)

#include <iostream>
#include <string>

class C
{
public:
  C(const std::string& p_s) : s(p_s) {}
  const std::string& s;
};

int main()
{
  #define x1 C(std::string("hello1")) // Line 1
  std::cout << x1.s << std::endl; // Line 2

  const C& x2 = C(std::string("hello2")); // Line 3
  std::cout << x2.s << std::endl; // Line 4
}

澄清:

请注意,我相信 Boost uBLAS 存储引用,这就是我不想存储副本的原因。如果您建议我按值存储,请解释为什么 Boost uBLAS 是错误的,按值存储不会影响性能。

【问题讨论】:

  • 这与定义无关,而是存储对临时对象的引用!

标签: c++ expression-templates c++11


【解决方案1】:

通过引用存储的表达式模板通常这样做是为了提高性能,但需要注意的是,它们仅用作临时对象

取自Boost.Proto的documentation(可用于创建表达式模板):

注意 精明的读者会注意到上面定义的对象 y 将保留对临时 int 的悬空引用。在 Proto 处理的高性能应用程序中,通常会在任何临时对象超出范围之前构建和评估表达式树,因此通常不会出现这种悬空引用的情况,但肯定需要注意这一点. Proto 提供了用于深度复制表达式树的实用程序,因此它们可以作为值类型传递而无需担心悬空引用。

在您的初始示例中,这意味着您应该这样做:

std::cout << C(std::string("hello2")).s << std::endl;

这样C 临时地址永远不会超过std::string 临时地址。或者,您可以将 s 设为非参考成员,正如其他人指出的那样。

既然您提到了 C++11,我希望将来表达式树能够按值存储,使用移动语义来避免昂贵的复制,并且像 std::reference_wrapper 这样的包装器仍然提供按引用存储的选项。这将与auto 很好地配合。

您的代码可能的 C++11 版本:

class C
{
public:
    explicit
    C(std::string const& s_): s { s_ } {}

    explicit
    C(std::string&& s_): s { std::move(s_) } {}

    std::string const&
    get() const& // notice lvalue *this
    { return s; }

    std::string
    get() && // notice rvalue *this
    { return std::move(s); }

private:
    std::string s; // not const to enable moving
};

这意味着像C("hello").get() 这样的代码只会分配一次内存,但仍然可以很好地使用

std::string clvalue("hello");
auto c = C(clvalue);
std::cout << c.get() << '\n'; // no problem here

【讨论】:

  • 感谢您的回答。但是,即使使用 std::move,按值存储也是性能问题吗?表达式对象在组合时可能会变得非常大,特别是如果它们是 std::array 的,例如,我看不到 std::move 的帮助(在这种情况下你仍然需要做一个副本,因为它不是堆分配的指针)。
  • 恐怕没有一体化的解决方案。上面允许使用表达式作为临时结果和存储中间结果,但确实依赖于s 成员具有可接受的大小并且移动构造足够便宜(std::move 毕竟没有魔法)。但是请注意,对于真正的表达式模板,通常可以注意“做正确的事情”:例如,使用类似std::decay 的特征工具来存储T* 而不是T[n]。这也适用于std::array。我个人会依赖std::array 的默认移动构造函数。
【解决方案2】:

但是有没有办法在不使用#define 的情况下避免这个问题?

是的。

将你的类定义为:(不存储引用)

class C
{
public:
  C(const std::string & p_s) : s(p_s) {}
  const std::string s; //store the copy!
};

保存副本!

演示:http://www.ideone.com/GpSa2


您的代码的问题是std::string("hello2") 创建了一个临时对象,只要您在C 的构造函数中,它就会一直存在,之后该临时对象被销毁,但您的对象x2.s 仍然指向到它(死物)。

【讨论】:

  • Nawaz:对不起,我在最初的问题中并不清楚。请参阅当前的问题说明。
  • @Clinton:如果您想传递一个临时字符串,则无法创建副本。
  • @ildjarn:是的……看起来更好……谢谢你的补充。 :)
【解决方案3】:

修改后:

按引用存储有时危险且容易出错。只有当您 100% 确定变量引用在其死亡之前永远不会超出范围时,您才应该这样做。

C++ string 非常优化。在您更改字符串值之前,所有都将仅引用相同的字符串。要对其进行测试,您可以重载operator new (size_t) 并放置一个调试语句。对于同一字符串的多个副本,您会看到内存分配只会发生一次。

你的类定义不应该通过引用来存储,而是通过值来存储,

class C {
  const std::string s;  // s is NOT a reference now
};

如果这个问题是为了一般意义(不是特定于字符串),那么最好的方法是使用动态分配。

class C {
  MyClass *p;
  C() : p (new MyClass()) {}  // just an example, can be allocated from outside also
 ~C() { delete p; }
};

【讨论】:

  • @Nawaz,不,我错过了参考部分。已编辑它。谢谢
  • iammilind: std::string 就是一个例子。它很可能是“sizeof() 为 1000 的大而可怕的类,由许多其他大而可怕的类组成”。如果我真的应该按值存储,为什么 Boost uBLAS 按引用存储? Boost 在那里做错了吗?
  • @Clinton,在这种情况下,您可以简单地将其 new 向上并在类中存储一个指针。在析构函数中你可以delete 它。我已经更新了答案。
  • @iammilind:动态分配表达式模板?这会不会太慢了?
  • Until you change a string value, all will refer to the same string only.
【解决方案4】:

如果不查看 BLAS,表达式模板通常会大量使用您甚至不应该知道存在的类型的临时对象。如果 Boost 在他们的内部存储这样的引用,那么他们将遇到与您在此处看到的相同的问题。但是只要这些临时对象仍然是临时的,并且用户不存储它们以备后用,一切都很好,因为只要临时对象存在,它们引用的临时对象就会保持活动状态。诀窍是当中间对象变成用户存储的最终对象时,您执行深度复制。您在这里跳过了最后一步。

简而言之,这是一个危险的举动,只要你图书馆的用户不做任何愚蠢的事情,这是绝对安全的。除非您有明确的需求,并且您很清楚后果,否则我不建议您使用它。即便如此,可能还有更好的选择,我从来没有认真地使用过表达式模板。

顺便说一句,既然你标记了这个 C++0x,auto x = a + b; 似乎是你的代码用户可以做的那些“愚蠢”的事情之一,使你的优化变得危险。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-05-04
    • 2012-11-28
    • 2016-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-04
    相关资源
    最近更新 更多