将 std::pair<T1, T2> const& 转换为 std::pair<T1 const, T2> const& 是否安全?

17
在理论或实践中,将std::pair<T1, T2> const &重新解释为std::pair<T1 const, T2> const &是否安全,假设程序员没有故意进行像特化std::pair<T1 const, T2>这样奇怪的操作?

5
我喜欢那两个由高声望的C++程序员留下的空白答案,哈哈。 - GManNickG
6
我也讨厌 std::map ;) - Potatoswatter
2
虽然我不得不问(同意Nawaz):为什么? - GManNickG
3
因为作为一个容器,std::map 不允许你规定 value_type 是什么。它必须是 std::pair< key const, mapped >。如果使用不当,它会干扰你的程序设计并违反关注点分离原则。(因此,我怀疑最实际的解答应该解释如何适应 std::set 或其他容器。) 但这个问题足够真实,并且在已经“锁定”为 std::map 的设计中很常见,因此这不是一个 XY 问题。 - Potatoswatter
2
你可以尝试从另一个中构造一个 std::pair<T1 const, T2> q(p.first, p.second),看看编译器是否会消除复制。或者使用智能指针。 - Maxim Egorushkin
显示剩余19条评论
3个回答

8

这样做是不可移植的。

std::pair 的要求在第20.3条中列出。第17.5.2.3条澄清了:

第18到30条和附录D不指定类的表示,并且有意省略了类成员的规范。实现可以根据需要定义静态或非静态类成员,或两者都定义,以实现第18到30条和附录D中指定的成员函数的语义。

这意味着,一个实现可以合法地(尽管极其不可能)包含一个部分特化,例如:

template<typename T1, typename T2>
struct pair<T1, T2>
{
    T1 first;
    T2 second;
};

template<typename T1, typename T2>
struct pair<const T1, T2>
{
    T2 second;
    const T1 first;
};

需要注意的是,这些类型显然不兼容布局。根据规则,还可以在first和/或second之前包含其他非静态数据成员的变体。


现在,考虑已知布局的情况会更有趣一些。尽管Potatoswatter指出了DR1334,声称Tconst T不兼容布局,但标准提供了足够的保证,使我们能够在大部分情况下实现:

template<typename T1, typename T2>
struct mypair<T1, T2>
{
    T1 first;
    T2 second;
};

mypair<int, double> pair1;
mypair<int, double>* p1 = &pair1;
int* p2 = reinterpret_cast<int*>(p1); // legal by 9.2p20
const int* p3 = p2;
mypair<const int, double>* p4 = reinterpret_cast<mypair<const int, double>*>(p3); // again 9.2p20

然而,对于std::pair,我们无法应用9.2p20规则,因为我们不知道first实际上是初始成员,这一点没有明确说明。

有趣...那是唯一的障碍吗?如果我在我的实现上验证顺序没有改变(编译器也没有在内部更改它),那么我就可以继续了吗? - user541686
1
但是 firstsecond 不是私有成员(根据第17.5.2.3节的标题),它们也没有标记为 // exposition only。这表明对我来说 pair 是一个特殊情况。 - ecatmur
@ecatmur:如果规则只适用于具有私有可见性的成员,则会在规则中说明。但它没有这样做。然而,即使您以这种方式限制它,允许将私有非静态成员插入std::pair也会更改布局,并且在不同的特化中可能会以不同的方式进行。我认为最好相信规则说的是什么:第20条款中的类定义不限制布局,它们只是存在所需成员的列表。 - Ben Voigt
例如,C++11 §26.4非常具体地指定了complex<T>的布局,与本答案中§17.5.2.3是绝对事实的假设相矛盾。因此,可以证明这种绝对的实现自由假设不成立。考虑到这一点,§20.3.2直接指定了std::pair<T, U>的布局(在第一个成员后填充),未标记为阐述。 - Cheers and hth. - Alf
@Alf:你找到了非常证明规则的例外。关于将std::complex<T>视为T[2]数组的处理语义必须明确说明的事实,强烈支持解释17.5.2.3适用于18到30条款中列出的每个类定义。 - Ben Voigt
显示剩余5条评论

6

pair在标准的第20.3.2节中定义为具有以下数据成员:

template <class T1, class T2>
struct pair {
    T1 first;
    T2 second;
};

这意味着对于具体类型 T1T2pair<T1, T2>pair<const T1, T2>,它们分别保证拥有以下数据成员:

struct pair<T1, T2> {
    T1 first;
    T2 second;
};
struct pair<const T1, T2> {
    const T1 first;
    T2 second;
};

现在,如果T1T2都是标准布局,那么pair<T1,T2>pair<const T1,T2>都是标准布局。如上所述,根据DR1334,它们不是布局兼容 (3.9p11),但根据9.2p19,它们可以reinterpret_cast到它们各自的T1const T1第一个成员。根据9.2p13,T2第二个成员必须位于第一个成员之后(即具有更高的地址),并且根据1.8p5必须立即位于第一个成员之后,使对象在考虑对齐后连续(9.2p19)。

我们可以使用offsetof检查这一点(它适用于标准布局类型):

static_assert(offsetof(pair<T1, T2>, second) ==
    offsetof(pair<const T1, T2>, second), "!");

自从pair<T1, T2>pair<const T1, T2>具有相同的布局后,按3.9.2p3所述,在向前转换时进行转换并使用结果访问成员是有效的:

如果类型T的对象位于地址A处,则值为A的类型cv T*指针被认为是指向该对象的指针,无论该值如何获得。

因此,仅当std::is_standard_layout<std::pair<T1,T2>>::valuetrue时,reinterpret_cast才是安全的。

2
你看到Potatoswatter在问题上的评论和链接的缺陷报告了吗?他声称T1const T1实际上不是布局兼容的。我不确定我是否同意这个观点,但你需要一些证据来支持你的说法,即它们是布局兼容的。 - Ben Voigt
@BenVoigt 哦,没注意到。不过我认为我们可以在它们没有布局兼容的情况下进行管理。 - ecatmur
我认为1.8p5并不严格要求std::pair<T1, T2>std::pair<const T1, T2>具有相等的大小、相等的填充、相等的offsetof等。这两种类型可以具有不同数量的填充,仍然每个类型都“占用连续的存储字节”。 - aschepler
@aschepler:如果它们具有相同的成员并且是标准布局,那么就不会出现问题。但我认为根据17.5.2.3,它们不一定具有相同的成员。 - Ben Voigt

0
实际上,将类型转换为const应该是安全的,因为您正在重新解释为具有相同表示形式的对象。然而,反过来引入了未定义的行为(从const到非const)。
至于“理论”答案,我应该指出,C++标准不保证const / non-const对象具有相同的位表示形式。 const关键字保证“概念上的常量性”,这取决于实现。

当然,它保证了const和非const对象的相同表示。const T&可以绑定到任意一个对象。但是当一个或两个模板类型参数标记为const时,这并不排除std::pair具有不同布局的特化情况。也许有其他编程语言可以实现这一点吗? - Ben Voigt
这对是不专业化的,正如问题中所提到的。 - user1556013
这并不是问题所说的。它只是说程序员没有定义专业化。标准库实现者可能已经定义了。 - Ben Voigt

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