在模板中使用不完整的类型

Use of incomplete types in templates

如果在模板实例化时类型是完整的,那么在模板中使用不完整的类型是否合法?

如下

#include <iostream>

struct bar;

template <typename T>
struct foo {

    foo(bar* b) : b(b) {
    }
    
     void frobnicate() {
          b->frobnicate();
     }

     T val_;
     bar* b;
};

struct bar {
     void frobnicate() {
          std::cout << "foo\n";
     }
};

int main() {
    bar b;
    foo<int> f(&b);
    f.frobnicate();
    return 0;
}

Visual Studio 毫无怨言地编译了上面的内容。 GCC 发出警告 invalid use of incomplete type 'struct bar' 但编译。 member access into incomplete type 'bar'.

出现 Clang 错误

虽然 MSVC 和 GCC 的行为也符合标准,但 Clang 报告错误(与警告或保持沉默相反)是正确的。有关详细信息,请参阅@HolyBlackCat 的回答。

您发布的代码是 ill-formed NDR。不过,你想做的事情是可行的。

您可以像 non-template class 一样延迟模板成员函数的定义。很像 non-template classes,只要这些要求 bar 是完整类型的定义仅在 bar 完成后发生,一切都很好。

唯一的问题是您需要将方法显式标记为内联以避免 multi-TU 程序中的 ODR 违规,因为定义几乎肯定会在 header.[=13= 中]

#include <iostream>

struct bar;

template <typename T>
struct foo {

    foo(bar* b) : b(b) {
    }
    
    inline void frobnicate();

    T val_;
    bar* b;
};

struct bar {
     void frobnicate() {
          std::cout << "foo\n";
     }
};

template <typename T>
void foo<T>::frobnicate() {
     b->frobnicate();
}

int main() {
    bar b;
    foo<int> f(&b);
    f.frobnicate();
    return 0;
}

代码格式错误,无需诊断。

[temp.res.general]/6.4

The validity of a template may be checked prior to any instantiation.

The program is ill-formed, no diagnostic required, if:
...
— a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, ...


如果你绝对不能在模板前定义bar,有一个解决方法:你可以在模板参数上引入一个人为的依赖。

template <typename T, typename, typename...>
struct dependent_type {using type = T;};
template <typename T, typename P0, typename ...P>
using dependent_type_t = typename dependent_type<T, P0, P...>::type;

然后使用 dependent_type_t<bar, T> 而不是 bar

如果您想使用前向声明作为模板参数来自定义模板,您可以这样做(没有警告或错误):

template <typename T, typename = T>
class print_name;

所以当你进行部分特化时,你使用第二个非特化模板参数进行调用:

struct john;

template <typename T>
class print_name<john, T>
{
public:
    void operator()(const T& f) const
    {
        std::cout << f.name << std::endl;
    }
};

在这种情况下 T 并非不完整。但是当你实例化的时候print_name<john>,SFINAE会踢。

这是一个完整的例子:


#include <iostream>

template <typename T, typename = T>
class print_name;

struct john;

template <typename T>
class print_name<john, T>
{
public:
    void operator()(const T& f) const
    {
        std::cout << f.name << std::endl;
    }
};

struct slim;

template <typename T>
class print_name<slim, T>
{
public:
    void operator()(const T& f) const
    {
        std::cout << f.myName << std::endl;
    }
};


#include <string>

struct john
{
    std::string name;
};

struct slim
{
    std::string myName;
};

int main()
{
    print_name<john>{}(john{"John Cena"});
    print_name<slim>{}(slim{"Slim Shady"});
    return 0;
}

https://godbolt.org/z/czcGo5aaG