通过引用传递指向常量对象的指针

3

我一直在处理一个使用了大量双指针的项目,但发现其中存在一些错误。经过一番深入挖掘,我意识到问题出在将非const对象通过指向const对象的引用指针传递时,它最终以复制的方式传递,而不是引用传递。我不理解为什么会这样,因为引用只是别名,const部分应该只会在尝试在该范围内更改对象时导致错误。更加令人困惑的是,当通过指向const对象的双指针或仅是const对象的引用进行传递时,不会发生这种情况。有谁可以解释为什么会发生这种情况,以及这种特殊情况与我提到的其他情况有何不同吗?

#include <iostream>

void PassByConstPtrRef(int const *const &num_ptr_ref)
{
    std::cout << &num_ptr_ref << std::endl;
}

void PassByPtrRef(int *const &num_ptr_ref)
{
    std::cout << &num_ptr_ref << std::endl;
}

void PassByPtrPtr(int const *const *const num_ptr_ref)
{
    std::cout << num_ptr_ref << std::endl;
}

void PassByConstRef(int const &num)
{
    std::cout << &num << std::endl;
}

int main()
{
    int *num_ptr = new int{ 10 };
    int *const *num_ptr_ptr = &num_ptr;
    int *const &num_ptr_ref = *num_ptr_ptr;

    std::cout << num_ptr_ptr << " : " << &num_ptr_ref << std::endl; // is equal

    std::cout << num_ptr_ptr << " : ";
    PassByConstPtrRef(num_ptr_ref); // is not equal

    std::cout << num_ptr_ptr << " : ";
    PassByPtrRef(num_ptr_ref); // is equal

    std::cout << num_ptr_ptr << " : ";
    PassByPtrPtr(&num_ptr_ref); // is equal

    int foo = 4;
    int &bar = foo;

    std::cout << &foo << " : ";
    PassByConstRef(bar); // is equal
}

输出:

0x7ffeeb6d59c8:0x7ffeeb6d59c8

0x7ffeeb6d59c8:0x7ffeeb6d59b0

0x7ffeeb6d59c8:0x7ffeeb6d59c8

0x7ffeeb6d59c8:0x7ffeeb6d59c8

0x7ffeeb6d59ac:0x7ffeeb6d59ac


@churill 谢谢,我已经在底部添加了输出。 - Joshua Williams
@JaMiT 如果您有任何更正意见,我会非常高兴接受?这可能会让我少感到困惑。在来这里问问题之前,我只是试图自己弄清楚,所以我想分享一下我的想法。 - Joshua Williams
@JaMiT 这样做会让我的代码适用于 int 但不适用于类吗?它似乎在任何情况下都不起作用,我通过删除 const 来使原始代码工作。 - Joshua Williams
1
啊...我看到混淆的一个原因了。结果在gcc 9和gcc 10之间,以及clang 9和clang 10之间发生了变化。在10+版本中进行测试的人将看不到差异。 - JaMiT
@JaMiT 是的,这正是我的问题。MSVC也会产生OPs结果。这里有一个godbolt上的测试作为参考,展示了两个不同版本的编译器。 - Lukas-T
2个回答

2
这是一个令人困惑的问题。症状的直接原因是为了将num_ptr_ref传递给PassByConstPtrRef()而创建了一个临时对象。绑定到此临时对象的参数因此具有不同的地址。虽然当处理对象时这种现象是一种已知的危险,但当只有更严格的cv限定符("cv"代表"const-volatile")时,它似乎在指针中发生很奇怪。毕竟,数据是相同的;区别在于可以对数据执行什么操作。
更深层次的原因是C++17标准中使用的措辞。相关章节([dcl.init.ref])使用了“与...相同的类型”这个短语,而int const *int *并不是相同的类型。尽管顶层类型允许不同的cv限定符,但在更深层次上则不允许。因此,在您的情况下,标准实际上需要创建一个临时对象。(过去式是故意的。)

幸运的是,这种情况被认为是不可取的,正如defect report 2352所记录的那样。建议的解决方案是将“与”改为“类似”,这考虑了指向类型的cv限定符。还有其他措辞上的变化。在我看来,其他变化要么是切换到“类似”的结果,要么是整理措辞以减少未来缺陷的可能性。也许其他变化比我意识到的更重要,但重要的是,通过这些变化,在您的情况下不再需要临时对象。

这个缺陷的解决方案已经纳入了GCC 10和clang 10,但没有纳入早期版本。(我不知道哪个或哪些版本的MSVC具有此增强功能。)旧编译器会给出您的结果(不同的地址),而新编译器则不会。考虑这是升级的一个原因? :)


供参考,以下是缺陷报告中给出的示例。它利用返回值而不是参数,但原理相同。有一个指向int的指针和一个指向(常量)const int指针的引用,这个引用不能直接绑定到指针上。

In an example like

int *ptr;
const int *const &f() {
  return ptr;
}

What is returned is a reference to a temporary instead of binding directly to ptr.


这有点难懂,但我想我已经理解了要点。感谢您的解释,指引我找到了正确的资源。 - Joshua Williams
@JoshuaWilliams 我终于回来了。答案现在应该更容易理解了。(我不敢保证“容易”,但至少比之前“容易一些”;)) - JaMiT

0

你的问题让我感到困惑,所以我进行了调查。

基本上,它可以归结为以下代码以进行说明:

#include <iostream>

int main()
{
    int *num_ptr = new int{ 10 };
    int *const &num_ptr_ref = num_ptr;
    int *const *num_ptr_ptr = &num_ptr;

    // Using a reference
    int const *const &const_num_ptr_ref = num_ptr_ref;
    std::cout << num_ptr_ptr << " : " << &const_num_ptr_ref << std::endl;;

    // Using a pointer    
    int const *const *const const_num_ptr_ptr = num_ptr_ptr;
    std::cout << num_ptr_ptr << " : " << const_num_ptr_ptr << std::endl;;
}

https://www.onlinegdb.com/online_c++_compiler上尝试时,您会发现前两个地址不同,而后两个地址相同。

经过一些挖掘,我对情况的理解如下:

  1. 第一个赋值是引用初始化(https://en.cppreference.com/w/cpp/language/reference_initialization)
  2. 第二个赋值是多级指针资格转换(https://en.cppreference.com/w/cpp/language/implicit_conversion#Qualification_conversions)

这里与我相关的重要区别是,在引用绑定期间,如果引用的类型和分配的表达式不同,它可能涉及到"必要时实现临时材料化"(https://en.cppreference.com/w/cpp/language/implicit_conversion#Temporary_materialization),这显然是在这里发生的。

我链接的cppreference的不同相关部分有一定的交叉引用,所以很难完全确定我没有漏掉什么,但它似乎解释了这种行为。


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