10

允许编写更高效的 C++ 代码的 C++0x 改进之一是 unique_ptr 智能指针(太糟糕了,它不允许通过 memmove() 之类的操作移动:该提案未纳入草案)。

即将发布的标准中还有哪些其他性能改进?以下面的代码为例:

vector<char *> v(10,"astring");
string concat = accumulate(v.begin(),v.end(), string(""));

该代码将连接向量v中包含的所有字符串。这段简洁的代码的问题在于,accumulate() 复制了周围的东西,并且不使用引用。并且 string() 每次调用 plus 运算符时都会重新分配。因此,与优化良好的类比 C 代码相比,该代码的性能较差。

C++0x 是否提供了解决问题的工具,也许还有其他工具?

4

4 回答 4

14

是的,C++ 通过一种叫做移动语义的东西来解决这个问题。

基本上,如果该对象是临时对象,则它允许一个对象采用另一个对象的内部表示。例如,您通常可以只允许目标字符串采用源字符串的内部表示,而不是通过复制构造函数复制字符串中的每个字节。仅当源是 r 值时才允许这样做。

这是通过引入移动构造函数来完成的。它是一个构造函数,您知道 src 对象是一个临时对象并且正在消失。因此,目的地接受 src 对象的内部表示是可以接受的。

移动赋值运算符也是如此。

为了区分复制构造函数和移动构造函数,该语言引入了右值引用。一个类定义了它的移动构造函数来获取一个只绑定到右值(临时)的右值引用。所以我的班级会按照以下方式定义一些东西:

 class CMyString
 {
 private:
     char* rawStr;
 public:

     // move constructor bound to rvalues
     CMyString(CMyString&& srcStr) 
     {
         rawStr = srcStr.rawStr
         srcStr.rawStr = NULL;             
     }

     // move assignment operator 
     CMyString& operator=(CMyString&& srcStr) 
     {
         if(rawStr != srcStr.rawStr) // protect against self assignment
         {
             delete[] rawStr;
             rawStr = srcStr.rawStr
             srcStr.rawStr = NULL;
         }
         return *this;
     }

     ~CMyString()
     {
         delete [] rawStr;
     }
 }

是一篇关于移动语义和允许您执行此操作的语法的非常好的和详细的文章。

于 2009-06-10T13:01:48.497 回答
7

一种性能提升将是广义常量表达式,它由关键字 constexpr 引入。

constexpr int returnSomething() {return 40;}

int avalue[returnSomething() + 2]; 

这不是合法的 C++ 代码,因为 returnSomething()+2 不是常量表达式。

但是通过使用 constexpr 关键字,C++0x 可以告诉编译器该表达式是一个编译时常量。

于 2009-06-10T13:04:56.010 回答
1

对不起 - 你不能说是string concat = accumulate(v.begin(),v.end(), string("")); 必须重新分配的事实。一个简单的实现当然会。但是编译器非常允许在这里做正确的事情。

在 C++98 中已经是这种情况,C++0x 继续允许智能和愚蠢的实现。也就是说,移动语义将使智能实现更简单。

于 2009-06-10T14:32:48.017 回答
1
vector<string> v(10, "foo");
string concat = accumulate(v.begin(), v.end(), string(""));

在任何 C++ 标准中,这个例子都是糟糕的编程。它等价于:

string tmp;
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp
tmp = tmp + "foo"; //copy tmp, append "foo", then copy the result back into tmp

C++11 移动语义只会处理等式的“将结果复制回 tmp”部分。来自tmp的初始副本仍将是副本。这是一个经典的 Schlemiel the Painter 算法,但比strcatC 中使用的通常示例还要糟糕。

如果accumulate只是使用+=而不是,+那么=它将避免所有这些副本。

但是 C++11 确实为我们提供了一种更好的方法,同时保持简洁,使用 range for

string concat;
for (const string &s : v) { concat += s; }

编辑:我想一个标准库供应商可以选择accumulate在操作数上移动到+,所以tmp = tmp + "foo"会变成tmp = move(tmp) + "foo",这几乎可以解决这个问题。我不确定这样的实现是否会严格遵守。GCC、MSVC 和 LLVM 在 C++11 模式下都不会这样做。正如accumulate定义的那样,<numeric>可能会假设它仅设计用于数字类型。

编辑 2:从 C++20accumulate开始,已重新定义为move按照我之前编辑的建议使用。我仍然认为这是对仅设计用于算术类型的算法的可疑滥用。

于 2013-09-09T17:05:36.020 回答