映射迭代器对值的复制省略

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(用于内联查找和字符串比较)和 newmemcpy(对于返回的副本)。

clang trunk 似乎没有内联那么多。 map::find 仍然存在,但仍然只有一次调用 newmemcpy

在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->second2.

变为 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中略有改动,但规则基本相同。