unique_ptr什么时候需要完整类型?

11
在下面的代码中,函数f()可以调用不完整的class Cunique_ptr<C>operator bool()operator *()成员函数。然而,当函数g()尝试为unique_ptr<X<C>>调用相同的成员函数时,编译器突然需要一个完整的类型并尝试实例化X<C>,然后失败了。由于某种原因,unique_ptr<X<C>>::get()不会导致模板实例化,并且像h()函数中所看到的那样编译正确。这是为什么?是什么让get()operator bool()operator *()不同?
#include <memory>

class C;
std::unique_ptr<C> pC;

C& f() {
    if ( !pC ) throw 0; // OK, even though C is incomplete
    return *pC;         // OK, even though C is incomplete
}

template <class T>
class X
{
    T t;
};

std::unique_ptr<X<C>> pX;

X<C>& g() {
    if ( !pX ) throw 0; // Error: 'X<C>::t' uses undefined class 'C'
    return *pX;         // Error: 'X<C>::t' uses undefined class 'C'
}

X<C>& h() {
    if ( !pX.get() ) throw 0; // OK
    return *pX.get();         // OK
}

class C {};

我在Visual Studio 2013中编译你的代码没有问题。你用的是什么编译器? - Ionel POP
每一行带有“error”的代码单独使用会出错吗?还是只有在同一个函数中有两个错误才会出错? - anatolyg
是的,每一行都出现了错误。 - Barnett
@Barry,您链接的重复问题中提供的答案对我来说过于深奥。 我只是要使用解决方法(例如 h()),并希望不会出现任何问题。 - Barnett
@RichardHodges 尽管是同一个问题,它将有相同的答案。如果有不同的答案,应该在那个问题下发表。此外,这里的任何答案都没有尝试回答这个问题。 - Barry
显示剩余8条评论
2个回答

7
以下是一个人为简化的例子,仅使用我们自己的类型:

这里是一个人为简化的例子,仅使用我们自己的类型:

class Incomplete;

template <class T>
struct Wrap {
    T t;
};

template <class T>
struct Ptr {
    T* p;

    void foo() { }
};

template <class T>
void foo(Ptr<T> ) { }

int main() {
    Ptr<Incomplete>{}.foo();         // OK
    foo(Ptr<Incomplete>{});          // OK

    Ptr<Wrap<Incomplete>>{}.foo();   // OK
    ::foo(Ptr<Wrap<Incomplete>>{});  // OK!
    foo(Ptr<Wrap<Incomplete>>{});    // error
}

问题在于,当我们对foo进行非限定调用时,而不是对::foo进行限定调用或调用成员函数Ptr<T>::foo()时,我们会触发参数相关的查找(Argument-Dependent Lookup,ADL)。
ADL将查找类模板特化中模板类型相关联的命名空间,这将触发隐式模板实例化。需要触发模板实例化以执行ADL查找,因为例如Wrap<Incomplete>可以声明一个friend void foo(Ptr<Wrap<Incomplete>>),需要调用它。或者Wrap<Incomplete>可能有依赖基类,其命名空间也需要考虑。此时进行实例化会使代码不合法,因为Incomplete是一个不完全类型,不能有不完全类型的成员。
回到最初的问题,对!pX*pX的调用会调用ADL,导致实例化X<C>,这是不合法的。对pX.get()的调用不会调用ADL,这就是为什么它可以正常工作的原因。
更多细节请参见此答案,以及CWG 557

1
那么为什么我们在!pC处也不必进行重载决策呢?此时,C也没有完成。 - Richard Hodges
2
@RichardHodges 我们确实会对!pC进行重载决议。在此过程中没有要求C必须完整 - 它不是一个模板(不会被实例化)。而!pX则需要实例化X<C>,这是非法的。 - Barry
2
@RichardHodges 过载解析的第一步是查找所有名称。查找名称会调用 ADL。ADL 触发实例化。 - Barry
明白了,非常感谢。^^ 这应该是答案。特别提到,在某些运算符的特殊规则下,cppreference页面可能会误导。 - Richard Hodges
@Barry我不太了解ADL,所以在回来之前我得去看看,但为了完整起见,您能否在您的示例中添加另一个foo(),以便在模板实例化后调用它? - Barnett
显示剩余2条评论

5

需要完整类型的不是 unique_ptr,而是你的类 X

std::unique_ptr<C> pC;

您实际上还没有为C进行任何分配,因此编译器在这里不需要知道C的具体细节。

std::unique_ptr<X<C>> pX;

在这里,您将C用作X的模板类型。因为X包含类型为T的对象,而这里的TC,所以编译器需要知道在实例化X时分配什么。(t是一个对象,因此在构造时实例化)。将T t;更改为T* t;,编译器就不会抱怨了。
编辑:

这并没有解释为什么h()可以编译通过,而g()不能。

这个示例编译得很好:
#include <memory>

class C;
std::unique_ptr<C> pC;

C& f() {
    if (!pC) throw 0; // OK, even though C is incomplete
    return *pC;         // OK, even though C is incomplete
}

template <class T>
class X
{
    T t;
};

std::unique_ptr<X<C>> pX;

typename std::add_lvalue_reference<X<C>>::type DoSomeStuff() // exact copy of operator*
{
    return (*pX.get());
}

void g() {
    if ((bool)pX) return;
}

class C {};

int main()
{
    auto z = DoSomeStuff();
}

这使得它更有趣,因为它模拟了operator* ,但编译成功。从表达式中删除!也可以。这似乎是多个实现(MSVC、GCC、Clang)中的一个bug。

1
正如Ivan所说,只要具有模板实例化的类不使用类型为T的直接对象(unique_ptr使用T*,而X使用一个),那么就很好。 - Hatted Rooster
7
为什么 h() 可以通过编译,而 g() 却不能? - Barnett
1
@M.M unique_ptr 可以接受不完整类型,因为它只有指向这些类型的指针。 - Ivan Rubinson
@Gill Bates 这不是我得到的结果。 h() 在没有 g() 的情况下能够编译通过。你使用的是哪个编译器? - Barnett
@barnett 你试着调用它了吗?这就是我所说的“使用”它。 - Hatted Rooster
显示剩余15条评论

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