映射迭代器对值的复制省略
Copy elision of map iterator pair value
在下面的 MVE 中,Get
函数的 return 值是否符合复制省略的条件?
编辑
我稍微改变了这个例子。在 Debug 和 Release 版本中使用 Visual Studio 2017,我在 return 语句中看到一个复制结构。希望这只是因为我弄乱了帮助我进行调试的 Type
。
#include <map>
#include <string>
#include <iostream>
#include <ostream>
struct Type
{
Type()
{
std::cout << "Default construction\n";
};
explicit Type(std::string obj) : obj(std::move(obj))
{
std::cout << "Other construction\n";
}
~Type() = default;
Type(const Type& other) : obj{other.obj}
{
std::cout << "Copy construction\n";
}
Type(Type&& other) noexcept : obj{std::move(other.obj)}
{
std::cout << "Move constructor\n";
}
Type& operator=(const Type& other)
{
std::cout << "Copy assignment\n";
if (this == &other)
return *this;
obj = other.obj;
return *this;
}
Type& operator=(Type&& other) noexcept
{
std::cout << "Move assignment\n";
if (this == &other)
return *this;
obj = std::move(other.obj);
return *this;
}
friend std::ostream& operator<<(std::ostream& os, const Type& obj1)
{
return os << obj1.obj;
}
std::string obj;
};
std::map<std::string, Type> mVariables;
Type Get(const std::string& variableName)
{
const auto variableIt = mVariables.find(variableName);
if(variableIt==std::end(mVariables)) {
throw std::runtime_error("Unknown variable requested.");
}
return variableIt->second;
}
int main()
{
mVariables.emplace(std::make_pair("key", Type("value")));
const auto value = Get("key");
std::cout << value;
return 0;
}
上面的示例提供了以下输出,它提出了一些关于 make_pair
的问题,但这不是这里的讨论。我想我的困惑是,在这个例子中是什么阻止了复制省略的发生?
Other construction
Move constructor
Move constructor
Copy construction
value
我建议你玩Compiler Explorer。
我在 GCC trunk 中看到的是,几乎所有的映射和字符串函数都被内联,剩下的函数调用只有 memcmp
(用于内联查找和字符串比较)和 new
和 memcpy
(对于返回的副本)。
clang trunk 似乎没有内联那么多。 map::find
仍然存在,但仍然只有一次调用 new
和 memcpy
。
在C++171之前,const auto value = Get("key");
的语义是copy-initializing来自返回表达式variableIt->second
的临时对象,然后copy-initializing value
来自临时对象。所以本来基本就是两个copies/moves.
临时对象中的copy/move可以根据N3797[class.copy]第31段第3条直接从variableIt->second
构造value
来省略:
- when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.
此副本自 C++17 起通过防止临时对象在语义上具体化来保证。 const auto value = Get("key");
的新语义从 variableIt->second
2.
变为 copy-initializing value
来自variableIt->second
的复制不能被省略,因为它不符合出现在return
语句中的复制省略要求,即N3797 [class.copy] 第 31 段项目符号 13:
- in a
return
statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
这是合理的,因为variableIt->second
的生命周期没有结束,可能会在未来被使用,所以value
不能优化为variableIt->second
的别名,因此一个副本是必要的。
1自 C++17 以来的唯一区别是第三段中提到的保证复制省略。从C++17之前的语义开始分析更直接(在我看来)。
2在C++17中有几个规则结合起来得出这个结论,对于这道题来说并不重要,所以我引用标准中的相应规则。
3这条规则的措辞在C++17中略有改动,但规则基本相同。
在下面的 MVE 中,Get
函数的 return 值是否符合复制省略的条件?
编辑
我稍微改变了这个例子。在 Debug 和 Release 版本中使用 Visual Studio 2017,我在 return 语句中看到一个复制结构。希望这只是因为我弄乱了帮助我进行调试的 Type
。
#include <map>
#include <string>
#include <iostream>
#include <ostream>
struct Type
{
Type()
{
std::cout << "Default construction\n";
};
explicit Type(std::string obj) : obj(std::move(obj))
{
std::cout << "Other construction\n";
}
~Type() = default;
Type(const Type& other) : obj{other.obj}
{
std::cout << "Copy construction\n";
}
Type(Type&& other) noexcept : obj{std::move(other.obj)}
{
std::cout << "Move constructor\n";
}
Type& operator=(const Type& other)
{
std::cout << "Copy assignment\n";
if (this == &other)
return *this;
obj = other.obj;
return *this;
}
Type& operator=(Type&& other) noexcept
{
std::cout << "Move assignment\n";
if (this == &other)
return *this;
obj = std::move(other.obj);
return *this;
}
friend std::ostream& operator<<(std::ostream& os, const Type& obj1)
{
return os << obj1.obj;
}
std::string obj;
};
std::map<std::string, Type> mVariables;
Type Get(const std::string& variableName)
{
const auto variableIt = mVariables.find(variableName);
if(variableIt==std::end(mVariables)) {
throw std::runtime_error("Unknown variable requested.");
}
return variableIt->second;
}
int main()
{
mVariables.emplace(std::make_pair("key", Type("value")));
const auto value = Get("key");
std::cout << value;
return 0;
}
上面的示例提供了以下输出,它提出了一些关于 make_pair
的问题,但这不是这里的讨论。我想我的困惑是,在这个例子中是什么阻止了复制省略的发生?
Other construction
Move constructor
Move constructor
Copy construction
value
我建议你玩Compiler Explorer。
我在 GCC trunk 中看到的是,几乎所有的映射和字符串函数都被内联,剩下的函数调用只有 memcmp
(用于内联查找和字符串比较)和 new
和 memcpy
(对于返回的副本)。
clang trunk 似乎没有内联那么多。 map::find
仍然存在,但仍然只有一次调用 new
和 memcpy
。
在C++171之前,const auto value = Get("key");
的语义是copy-initializing来自返回表达式variableIt->second
的临时对象,然后copy-initializing value
来自临时对象。所以本来基本就是两个copies/moves.
临时对象中的copy/move可以根据N3797[class.copy]第31段第3条直接从variableIt->second
构造value
来省略:
- when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.
此副本自 C++17 起通过防止临时对象在语义上具体化来保证。 const auto value = Get("key");
的新语义从 variableIt->second
2.
value
来自variableIt->second
的复制不能被省略,因为它不符合出现在return
语句中的复制省略要求,即N3797 [class.copy] 第 31 段项目符号 13:
- in a
return
statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
这是合理的,因为variableIt->second
的生命周期没有结束,可能会在未来被使用,所以value
不能优化为variableIt->second
的别名,因此一个副本是必要的。
1自 C++17 以来的唯一区别是第三段中提到的保证复制省略。从C++17之前的语义开始分析更直接(在我看来)。
2在C++17中有几个规则结合起来得出这个结论,对于这道题来说并不重要,所以我引用标准中的相应规则。
3这条规则的措辞在C++17中略有改动,但规则基本相同。