【问题标题】:In C++ template function, why does dependent function call give "not declared" error?在 C++ 模板函数中,为什么依赖函数调用会给出“未声明”错误?
【发布时间】:2023-03-29 06:28:02
【问题描述】:

在 C++ 模板函数 foo() 中,调用 ::bar(TT*) 在 gcc 4.4.3 下会出现以下错误:

g++ -o hello.o -c -g hello.cpp
hello.cpp: In function 'void foo(std::vector<TT*, std::allocator<TT*> >&)':
hello.cpp:8: error: '::bar' has not been declared

这是有问题的代码:

// hello.cpp

#include <vector>

template<typename TT> void foo(std::vector<TT*> &vec)
{
    TT *tt;
    ::bar(tt);
    vec.push_back(tt);
}

class Blah
{
};

void bar(Blah *&)
{
}

int main(int argc, char *argv[])
{
    std::vector<Blah*> vec;
    foo(vec);

    return 0;
}

C++ 区分依赖于模板参数(此处为 TT)的符号和那些独立且可以立即求值的符号。

显然,编译器认为我的 ::bar(TT*) 调用是独立的,并尝试立即解决它。同样清楚的是,该函数调用 依赖于 TT,因为该函数调用采用 TT* 类型的参数,因此编译器应该等到 foo(vec) 实例化才能解析 ::bar(TT* )。

这是一个 gcc 错误还是我错过了 C++ 模板的一些微妙之处?

编辑:这里有一个稍微复杂一点的例子,有两个版本的 ::bar() 来澄清声明顺序不是我的问题的问题。解析模板时,编译器无法知道下面的 main() 是用 TT=Blah 还是用 TT=Argh 实例化模板函数。因此,编译器最早应该在 line 35 line 28 之前(如果有的话)给出错误。但是 line 8 line 16 给出了错误。

编辑#2:改进了这个例子。

编辑#3:添加了对这个示例的更正以使其按需要工作。 bar(tt) 现在正确地引用 bar(Blah*)。理由如下。 (谢谢大家)。

// hello.cpp
#include <vector>

class XX {};
void bar(XX*) {}

class CC {
public:
    void bar();
    void bar(int *);
    void bar(float *);

    template<typename TT> static void foo(std::vector<TT*> &vec);
};

template<typename TT>
void CC::foo(std::vector<TT*> &vec) {
    using ::bar;
    TT *tt;
    bar(tt);
    vec.push_back(tt);
}

class Argh {};
void bar(Argh *&aa) { aa = new Argh; }

class Blah {};
void bar(Blah *&bb) { bb = new Blah; }

int main(int argc, char *argv[]) {
    std::vector<Blah*> vec;
    CC::foo(vec);
    return 0;
}

【问题讨论】:

  • 错误说“::bar”没有声明不是像“没有匹配的原型”......也许bar真的没有声明?这是一个猜测
  • "同样清楚的是,该函数调用依赖于 TT,因为该函数调用采用 TT* 类型的参数,因此编译器应该等到 foo(vec) 实例化才能解析 ::bar( TT*)。”。错误的。仅仅是名称“::bar”和“bar”是不相关的。有一个特殊规则可以使其他非依赖名称在用作函数时依赖,这是 @Chris 引用的文本,并且仅适用于非限定名称。无论如何,在实例化时需要做的是使用给定(依赖)参数对找到的声明进行重载决议。
  • 我已经在下面提到了这一点,但这似乎不同意你的观点:Stroustrup TC++PL Sp Ed,第 C.13.8.1 节,从属名称:“基本上,调用的函数的名称是如果通过查看它的参数或它的形式参数,它显然是依赖的”。按照这个标准,::bar“明显依赖”。

标签: c++


【解决方案1】:

目前还没有人指出我不能指出当前标准的任何部分。

C++03 不会使名称 ::bar 依赖。依赖类型的类型名称发生依赖关系,依赖类型或值的依赖表达式的非类型名称发生依赖关系。如果在依赖类型中查找名称,它将成为依赖类型的 id 表达式(最后一个项目符号 14.6.2.2/3),并且它的查找会延迟到实例化。名称::bar 没有这样的依赖表达式。如果你打电话给bar(tt),C++03 在 14.2.6 的一个特殊规则说

在一个表达式中:

postfix-expression ( expression-listopt )

其中后缀表达式是一个标识符,当且仅当表达式列表中的任何表达式是一个依赖于类型的表达式 (14.6.2.2) 时,该标识符才表示一个依赖名称。

因此,您需要删除 :: 以使其成为标识符并使​​其依赖于此特殊规则。

我不能删除 :: 的原因是在我的真实代码中,模板函数 foo 是类 CC 的成员函数,并且存在一系列重载成员函数 CC::bar(. ..),这意味着我需要限定 ::bar(TT*) 以避免默认为 CC::bar(...)。这就是 :: 存在的目的,如果标准说我不能在这里使用 :: ,我会感到惊讶

解决它的正确方法是将 using 声明引入函数的本地范围。

namespace dummies { void f(); }
template<typename T>
struct S {
  void f();
  void g() { 
    using dummies::f; // without it, it won't work
    f(T()); // with ::f, it won't work
  }
};

struct A { };
void f(A) { } // <- will find this

int main() {
  S<A> a;
  a.g();
}

如果普通查找找到类成员函数,ADL 将不会做任何事情。因此,你引入了 using 声明,所以普通查找找不到类成员函数,而 ADL 可以在实例化时将声明提前可见。

但这似乎不同意您的观点:Stroustrup TC++PL Sp Ed,第 C.13.8.1 节,从属名称:“基本上,如果通过查看,被调用函数的名称显然是依赖的,则它是依赖的在它的论点或在它的形参”

Stroustrup 的书也是为可能还不了解 C++ 的人编写的。它不会像这些书一样尝试以 100% 的准确度涵盖所有规则。血淋淋的细节留给 ISO 标准的读者。

另外,函数的形参与函数调用是否依赖无关。在 IS 中,只有实际参数定义函数名的依赖关系。这在 an old draft from 1996 中有所不同,它具有 implicit 和 explicit 依赖的概念。隐式依赖定义为

如果名称是函数,则名称隐含地依赖于模板参数 函数调用中使用的名称和函数调用将有一个差异 如果是类型、模板或枚举器,则提供解析或无解析 程序中缺少模板参数中提到的内容。

[...]

[示例:一些依赖于模板参数类型 T 的调用是:

  1. 被调用的函数有一个依赖于 T 的参数,根据 类型扣除规则(temp.deduct)。例如,f(T), f(Array) 和 f(const T*)。

  2. 实际参数的类型取决于 T。例如,f(T(1)), f(t)、f(g(t)) 和 f(&t) 假设 t 的类型为 T。

还给出了一个实际的例子

这个格式错误的模板实例化使用了一个不 依赖于模板参数:

template<class T> class Z {
public:
        void f() const
        {
                g(1); // g() not found in Z's context.
                      // Look again at point of instantiation
        }
};

void g(int);
void h(const Z<Horse>& x)
{
        x.f(); // error: g(int) called by g(1) does not depend
               // on template-argument ``Horse''
}

调用 x.f() 产生特化:

void Z<Horse>::f() { g(1); }

调用 g(1) 将调用 g(int),但由于该调用不依赖于 在模板参数 Horse 上,因为 g(int) 不在范围内 模板的定义点,调用 x.f() 是病态的 形成。

另一方面:

void h(const Z<int>& y)
{
        y.f(); // fine: g(int) called by g(1) depends
               // on template-argument ``int''
}

这里,调用 y.f() 产生了特化:

void Z<int>::f() { g(1); }

调用 g(1) 调用 g(int),因为调用依赖于 tem- 板参数 int,调用 y.f() 是可以接受的,即使 g(int) 不在模板定义的范围内。 ]

这些东西都留给历史了,即使它的最后痕迹也在慢慢消失,尽管不是主动驱动的(例如,n3126 摆脱了 [temp.names]/p4 处的“显式依赖”作为另一个变化,因为“显式依赖”和“隐式依赖”之间的区别在 IS 中从未存在)。

【讨论】:

  • 关于@Chris showed you the part of the Standard that:@Chris 向我展示了尚未竞争的 C++0x 标准中的一个措辞,@Alf 很快指出它与当前的真正标准不同——我的代码是正确的(根据 C++98 §14.6.2)。另外,我的代码在直觉上是正确的。到目前为止,无论出于何种原因,限定都不应影响模板符号的依赖性或独立性。在实例化点,编译器有大量信息可以正确解析 ::bar(TT*)。
  • 关于ADL,你是说我应该在模板函数foo的顶部添加using ::bar吗?因为我试过了,但它在 gcc 上不起作用。
  • @Shawn 一个虚拟的bar 函数需要在您的模板之前声明,然后您可以通过这样的使用声明将其带入本地范围。关于 14.6.2:向我们展示您如何根据该段落得出您的代码有效的结论(通过修改您的问题)。如果您只是告诉我们您的代码根据它是有效的,那将无济于事。我已经向您展示了 14.6.2 仅适用于标识符,而“::bar”不是标识符。我还向您展示了 ADL 不会对限定名称执行任何操作。如果你拒绝我说的话,我帮不了你。
  • 我正在详细研究您的信息,但我承认我不明白为什么人们不断提出 C++0X。如果您或任何人可以在原始问题中对我的第二个代码示例进行简单修改以提供我想要的行为,这可能会有所帮助。或者,如果您可以告诉我标准在哪里说无法获得我想要的行为,那么我会接受这个答案,但这意味着 C++ 模板的设计严重错误。我尝试添加void bar(int*) {} 和using ::bar;,但没有成功。编译器仍然过早地解析 bar。
  • 如果我调用bar(TT*) 而不是::bar(TT*),现在可以使用using ::bar。如果我理解正确:模板内的函数调用包含模板参数(并且是依赖表达式)是不够的,C++ 不会总是等待绑定函数调用,因为也许您想传递延迟绑定的模板参数进入一个立即绑定的函数调用。在我看来,应用 ::operator 不是一个有效的解决方法似乎是不正当的,但在创建一个虚拟函数以将 C++ 重载解析规则踢到正确模式时确实存在一个可用的解决方法。
【解决方案2】:

来自 C++ 规范的第 14.7.2 节:

在一个表达式中:

    postfix-expression ( expression-listopt )

如果 postfix-expression 是 unqualified-id 但不是 template-id , unqualified-id 表示依赖 当且仅当表达式列表中的任何表达式是依赖于类型的表达式 (14.7.2.2) 时才命名。

因为::b 不是非限定ID,所以它不是依赖名称。如果您删除 ::,它是一个非限定名称,因此也是一个从属名称。对于非依赖名称,查找发生在模板声明处,而不是实例化处,此时没有可见的bar 全局声明,因此会出现错误。

【讨论】:

  • 编译器总是需要检查一个名字是否已经被声明,不管它是否依赖。名称查找和重载解析是两个不同的东西。前者发生在模板定义时,后者发生在实例化时。
  • -1 C++98 标准中没有这样的措辞。对 C++03 进行了更正,但我不知道是什么。我最好的猜测是你看过一些 C++0x 草案。在 C++98 标准中,它的第 14.6.2 节说:“在 postfix-expression ( expression-list ) 形式的表达式中,其中 postfix-expression 是一个 identifier,该标识符表示一个 dependent-name 当且仅当 expression-list 中的任何表达式是一个依赖于类型的表达式(14.6.2.2)”。让我检查一下...是的,您的措辞出现在 C++0x 草案的 §14.***6***.2 中。呃,我试图删除downvote
  • @Alf 有趣的是,最新的 n3126 草案确实说 id-expression 而不是 unqualified-id 或标识符。这已由 #448 更改。但它不会对提问者的代码做任何事情:尽管名称将变得依赖,但不会找到声明,因为 ADL 不会对限定名称做任何事情。
  • 很有趣,但是 Stroustrup TC++PL Sp Ed, Section C.13.8.1, Dependent Names 中的这个呢:“基本上,如果调用的函数的名称明显依赖于查看它的参数或形式参数”。
  • 仅供参考,我无法删除 :: 的原因是在我的真实代码中,模板函数 foo 是 CC 类的成员函数,并且存在一系列重载成员函数 CC: :bar(...),这意味着我需要限定 ::bar(TT*) 以避免默认为 CC::bar(...)。这就是 :: 存在的意义,如果标准说我不能在这里使用 :: 我很惊讶,而且还没有人指出当前标准的任何部分说我不能。
【解决方案3】:

这种丑陋适用于英特尔 C++ 11.0,或许可以说明编译器的观点:

#include <vector>
#include <iostream>

// *********************
// forward declare in the global namespace a family of functions named bar
// taking some argument whose type is still a matter of speculation
// at this point
template<class T>
void bar(T x);
// *********************

template<typename TT>
void foo(std::vector<TT*> &vec)
{
   TT *tt;
   ::bar(tt);
   vec.push_back(tt);
}

class Blah
{
  public:
};

void bar(Blah *x)
{
  // I like output in my examples so I added this
  std::cout << "Yoo hoo!" << std::endl;
}

// **********************
// Specialize bar<Blah*>
template<>
inline
void bar<Blah*>(Blah *x) { ::bar(x); }
// **********************

int main(int, char *)
{
  std::vector<Blah*> vec;
  foo(vec);

  return 0;
}

【讨论】:

    【解决方案4】:

    这应该可以,你的问题是声明顺序:

    // hello.cpp
    
    #include <vector>
    
    
    class Blah
    {
    public:
    
    };
    
    void bar(Blah *&)
    { }
    
    template<typename TT> void foo(std::vector<TT*> &vec)
    {
        TT *tt;
        ::bar(tt);
        vec.push_back(tt);
    }
    
    
    int main(int argc, char *argv[])
    {
        std::vector<Blah*> vec;
        foo(vec);
    
        return 0;
    }
    

    【讨论】:

    • 另外值得注意的是,由于他没有在 bar() 中提供任何不是 Blah*& 的参数,因此只有可隐式转换为 Blah*& 的类型才能被接受为 TT*,这几乎就是废话*。
    • 不,这个答案不正确。 bar() 可能有许多版本,它们的一个参数采用不同类型的指针。其中一些甚至还没有被发明出来。这就是涉及模板的全部意义所在。
    • 但是 bar() 的版本并不多。只有一个。为什么模板函数调用具有模板类型相关参数的非模板函数?您只是将模板参数的范围限制为非模板函数接受的范围,违背了它们的目的。
    • @DeadMG:我认为你没有抓住重点。问题不在于bar 的“许多版本”,而在于为什么bar 需要在模板之前定义,即使模板直到main 才被实例化。
    • @Shawn:在 C++0x FCD 中是 14.6.3 [temp.nondep]。
    【解决方案5】:

    依赖名称仍然需要函数声明。任何具有该名称的函数声明都可以,但必须有一些东西。编译器将如何从例如拼写错误的仿函数对象中消除重载函数调用的歧义?换句话说,找到该名称的 some 函数,从而验证该名称是否可以重载,在限定(或非限定)名称查找中启动重载解析过程。

    // hello.cpp
    
    #include <vector>
    
    void bar(); // Comeau bails without this.
    
    template<typename TT> void foo(std::vector<TT*> &vec)
    {
        TT *tt;
        ::bar(tt);
        vec.push_back(tt);
    }
    
    class Blah
    {
    };
    
    void bar(Blah *&)
    {
    }
    
    int main(int argc, char *argv[])
    {
        std::vector<Blah*> vec;
        //foo(vec); - instanting it is certainly an error!
    
        return 0;
    }
    

    【讨论】:

    • 在 gcc 4.4.3 下,您的代码在第 10 行 ::bar(tt) 处给出错误 "too many arguments to function 'void bar()'"。我对您的想法很感兴趣,即在模板解析期间符号栏需要部分绑定到一系列重载函数,但我发现如果为真,这种行为令人惊讶,并且至少在 gcc 下,如果 :: 我的代码有效被删除。 (此代码是一个简化的示例,对于我的真实代码,我需要::,所以请不要打扰建议。)
    • @Shawn:不知道该说什么。我没有尝试 GCC,我的评论是基于最近阅读了标准的那部分并尝试了 Comeau。我知道没有任何声明是非法的。我不确定是否允许 GCC 给你too many arguments,必须回去仔细阅读。 (不打算 :v( .)
    【解决方案6】:

    您的代码在 VS2005 上运行良好。 所以它确实看起来像是 gcc 中的一个错误。 (我不知道规范是怎么说的,也许有人会过来发布它。)

    至于gcc,我也试过在foo之前定义一个不同的bar重载,这样就至少定义了符号bar:

    void bar(float *) {
    }
    
    template<typename TT> void foo(std::vector<TT*> &vec)
    {
        TT *tt;
        ::bar(tt);
        vec.push_back(tt);
    }
    
    void bar(int *) {
    }
    
    int main(int argc, char *argv[])
    {
        std::vector<int*> vec;
        foo(vec);
    
        return 0;
    }
    

    但 gcc 仍然对 int * 重载完全视而不见:

    错误:无法将参数 1 的 int* 转换为 float* 到 void bar(float*)

    gcc 似乎只会使用那些在模板本身之前定义的函数。

    但是,如果您删除显式 :: 说明符并将其改为 bar(tt),它似乎可以正常工作。 Chris Dodd 的回答似乎令人满意地解释了为什么会这样。

    【讨论】:

    • 我怀疑这里的 VC++ 是错误的。它根本不关心模板和依赖/非依赖事物的两阶段编译。 Comeau 同意 GCC 的观点。 - 至于删除::,我想这会留下一个选项,即稍后将使用参数相关查找找到合适的bar(也许它会在T所属的命名空间中找到)。
    猜你喜欢
    • 1970-01-01
    • 2011-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多