C++ std::tuple对象的销毁顺序

52

在std::tuple成员销毁的顺序方面,是否有规则?

例如,如果Function1返回一个std::tuple<std::unique_ptr<ClassA>, std::unique_ptr<ClassB>>Function2,那么我是否可以确信(当离开Function2作用域时)第二个成员所引用的ClassB的实例会在第一个成员所引用的ClassA实例之前被销毁?

std::tuple< std::unique_ptr< ClassA >, std::unique_ptr< ClassB > > Function1()
{
    std::tuple< std::unique_ptr< ClassA >, std::unique_ptr< ClassB > > garbage;
    get<0>(garbage).reset( /* ... */ );
    get<1>(garbage).reset( /* ... */ );
    return garbage;
}

void Function2()
{
    auto to_be_destroyed = Function1();
    // ... do something else

    // to_be_destroyed leaves scope
    // Is the instance of ClassB destroyed before the instance of ClassA?
}

2
我猜这主要取决于你的标准库中 std::tuple 的实现方式。 - Arunmu
3
在规范中我找不到任何关于 std::tuple 销毁顺序的说明。可能应该将其视为未指定并进行归档。 - 101010
1
https://dev59.com/EF4c5IYBdhLWcg3wqb4C#27663655 - Howard Hinnant
2
这个问题最近在Antony Polukhin的精彩CPPCon 2016演讲中提出,他讲述了如何在C++14中实现部分反射而没有适当的语言支持。如果我没记错的话,他说唯一可怕的事情是不得不重新实现std::tuple以强制执行特定的初始化顺序。此外,在此提醒@101010。 - einpoklum
3个回答

61

针对你的问题,我不会给出一个明确的答案,而是提供一些我学到的人生经验:

如果你能为多种选择制定一个合理的论点,说明哪种选择应该成为标准 - 那么你不应该假定任何一种选择被指定了(即使其中一种碰巧被指定了)。

在元组的上下文中,请善待维护你代码的人,并且不要允许元组元素的销毁顺序潜在地干扰其他元素的销毁。那太恶劣了... 想象一下将需要调试这个东西的那个不幸的程序员。实际上,当你已经忘记从前的聪明技巧时,那个可怜的灵魂可能就是你自己几年后。

如果你绝对依赖于销毁顺序,也许你应该只使用一个适当的类,将元组的元素作为其数据成员(你可以为其编写一个析构函数,明确哪些事项需要以什么顺序发生),或者采用其他安排,以方便更明确地控制销毁过程。


2
这是非常重要的一课!在使用C++编程时,这个提示必须时刻存在于开发者的脑海中。 - hbobenicio
我不太明白这个。自从C++11字符串被要求具有连续的存储空间,但有一个合理的论点(在编写C++03时使用)不要求这样做。或者,如果您对该示例提出异议,请选择另一个标准做出判断的案例,这可能是合理的两种方式之一。那么,我不应该编写依赖于C++11或C++14加强的东西的代码吗?还是您的意思是,如果标准不清楚,那么不要指望下一个程序员像您一样严格遵守? - Steve Jessop
或者以一个故意愚蠢的例子来说,有一个合理的论点认为std::vector应该被称为std::dynamic_array,而std::valarray应该被称为std::vector。但我认为,在编写代码时,基于std::vector是标准所规定的内容是非常合理的:我们不能在不假设它的情况下使用它。因此,我认为我没有理解你的建议适用于哪些情况;-) - Steve Jessop
@SteveJessop:关于字符串示例,请注意,所有不假定字符串具有连续存储的代码在C++03中同样适用(当然,假设它不依赖于C++11功能)。如果您想依赖连续存储,请这样做,因为有很好的理由。想想<algorithm>代码库假定了多少内容,以及它的广泛适用性。仍然可以,我承认你可以在我的人生经验教训前加上“其他条件相等时……”。 - einpoklum
@SteveJessop:至于你那个愚蠢的例子,显然你不能不对你正在使用的库的API做出假设。但是如果你看到你的同事写了一个名为“vector.hpp”的文件,我不会急于假设他/她在里面有什么样的类。 - einpoklum
2
作为补充说明:如果您最终确实依赖于这样一个巧妙的技巧,请务必使用注释详细记录您所做的事情。请描述您正在利用的行为以及为什么要利用它,尽可能详细地描述,以便任何需要维护代码的人都知道他们正在处理什么。 - Justin Time - Reinstate Monica

38
标准没有指定std::tuple的销毁顺序。§20.4.1/p1规定:“使用两个参数实例化的元组类似于使用相同两个参数实例化的对。”这里的“类似”并非被解释为“相同”,因此不意味着std::tuple应具有其参数的相反销毁顺序。
考虑到std::tuple的递归性质,最有可能的是销毁顺序与其参数的顺序一致。
我还根据GCC BUG 66699的错误报告进行了假设,在讨论中证明了我上述的假设。
尽管如此,std::tuple的销毁顺序仍未指定。

16
方面. 使用Clang 3.4编译时,std::pair和2元素的std::tuple的销毁顺序相同;使用g++ 5.3编译时,它们的顺序相反,这可能主要是由于libstd++中std::tuple的递归实现造成的。总的来说,正如我在评论中所说的那样,它取决于实现。从BUG报告中可以看出:

Martin Sebor的评论如下:

由于std::pair成员的布局已经完全指定,因此它们初始化和销毁的顺序也是确定的。测试用例的输出反映了这个顺序。

std:stuple子对象的初始化(和销毁)顺序没有那么明确地指定。至少从我的阅读规范来看,任何特定的顺序都不是很明显。

使用libstdc++的std::tuple输出与std::pair相反的原因是,该实现依赖于递归继承,在反向顺序存储和构造元组元素:即存储最后一个元素的基类首先存储和构造,然后是每个派生类(每个派生类都存储最后的第N个元素)。

引用了标准[20.4.1节]的报告内容如下:

1 这个子句描述了提供元组类型的元组库,可以使用任意数量的参数来实例化类模板元组。每个模板参数指定元组中一个元素的类型。因此,元组是值的异构的、固定大小的集合。带有两个参数的tuple的实例与使用相同的两个参数的pair的实例类似。参见20.3节。

链接的bug中提出的反对意见是:

被描述为相似并不意味着它们在每个方面都是相同的。

细节。std::pair和std::tuple是具有不同要求的不同类。如果您认为它们在这方面需要以相同的方式行事(即,其子对象的定义顺序相同),则需要指出特定措辞来保证它。


3
你怎么知道 std::pair 元素的销毁顺序是相反的? - masoud
2
相似并不等同于完全相同。 - 101010
基于libcxx和libstd++中对std::pair的实现。 - Arunmu
2
@101010 我不知道,我认为identical与similar相似。 - Yakk - Adam Nevraumont
2
@yakk 非常相似,但不完全相同,类似于相似 :) - 101010
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接