今天我重载了<<我的一个班级的操作员:
#ifndef TERMINALLOG_HH
#define TERMINALLOG_HH
using namespace std;
class Terminallog {
public:
Terminallog();
Terminallog(int);
virtual ~Terminallog();
template <class T>
Terminallog &operator<<(const T &v);
private:
};
#endif
正如您所看到的,我在头文件中定义了重载运算符,然后我继续在我的.cc文件中实现它:
//stripped code
template <class T>
Terminallog &Terminallog::operator<<(const T &v) {
cout << endl;
this->indent();
cout << v;
return *this;
}
//stripped code
之后我使用我的新类创建了一个main.cpp文件:
#include "inc/terminallog.hh"
int main() {
Terminallog clog(3);
clog << "bla";
clog << "bla";
return 0;
}
我继续说道:
g++ src/terminallog.cc inc/terminallog.hh testmain.cpp -o test -Wall -Werror
/tmp/cckCmxai.o: In function `main':
testmain.cpp:(.text+0x1ca): undefined reference to `Terminallog& Terminallog::operator<< <char [4]>(char const (&) [4])'
testmain.cpp:(.text+0x1de): undefined reference to `Terminallog& Terminallog::operator<< <char [4]>(char const (&) [4])'
collect2: ld returned 1 exit status
BAM!一个愚蠢的链接器错误,我仍然不知道它来自哪里。我玩了一下,注意到我的重载操作符的实现在我的头文件中解决了所有问题。现在我比以前更加困惑了。
为什么我不能将重载运算符的实现放在我的.cc文件中?当我把它放在我的头文件中时,它为什么运行顺畅?
提前感谢困惑
ftiaronsem
答案 0 :(得分:4)
编译器必须看到实现能够使用该模板。通常这意味着你把它放在标题中。
答案 1 :(得分:1)
除了@ Bo的回答:你应该阅读C ++ FAQ Lite中的文章:http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12以及更进一步。
答案 2 :(得分:1)
可以将实现保留在cpp文件中,但是您需要为使用它的每种类型声明模板的使用。有关详细说明,请参阅Parashift C++ Faq。
在您的情况下,您必须在cpp文件中的某处写入该行:
template Terminallog &Terminallog::operator<<(const char* &v);
答案 3 :(得分:0)
模板具有特殊属性:可以将其“实现”视为其签名的一部分。
如果编译器在您的示例中仅看到行
Terminallog &operator<<(const T &v);
你可以使用operator&lt;&lt;绝对没有!我的字面意思是苹果和梅子。 那个经营者将是万能的!
然而,您的运营商IMPLEMENTATION需要一种可以通过管道进入cout的类型! 对于许多类型而言,情况并非如此!尝试将您的Terminallog类管道输入cout。 那不行。编译器只能检查这种合规性, 如果你告诉它你想用T做什么。
第二部分:
了解#include的作用。
C / C ++(遗憾地)知道接口和实现之间没有真正的区别,它没有模块系统。标题,cpps都只是一个惯例。你可以高兴地写
#include <something.cpp>
。没有什么能阻止你。
#include"inc/terminallog.hh"
语句只将头部的所有内容转储到当前文件中(以及所有内容#included)。这是我们使用包含保护a'la #ifndef TERMINALLOG_HH
的一个稍微令人沮丧的原因,因为像字符串或类似的标题可能会被转储到我们的文件中一百次或更多,并且编译器会不断地抛出重定义错误。
实际上,如果添加`#include,它很可能会删除你的错误,因为现在也会转储Terminallog的实现。
编译器一个接一个地执行实现文件,包括,
并在一个很长的通道中运行它们。然后它会忘记它刚刚做的所有事情,然后继续下一个实现文件,拉入包含,然后继续下去。
它首先编译Terminallog.cc,找到那里的所有内容的代码,以及模板的其余部分。它不会生成Terminallog :: operator&lt;&lt;(string),因为它从未在那里使用过。
从第1部分和第2部分开始,当编译器编译testMain.cpp时, 在dumpallog.hh中转储之后,这就是它必须使用的内容:
class Terminallog {
public:
Terminallog();
Terminallog(int);
virtual ~Terminallog();
template <class T>
Terminallog &operator<<(const T &v);
private:
};
int main() {
Terminallog clog(3);
clog << "bla";
clog << "bla";
return 0;
}
从编译器的角度来看,一切看起来都很酷。运营商LT;&LT;有一个模板参数,
所以编译器为operator<<(T = const string)
写一个调用标记,
constructor(int)
完全相同。它没有看到实施,也不关心。瞧,你的全能运营商。
它不知道T必须有一个运算符&lt;&lt;在这一点上有ostream!尽管如此,这是合法的,它只是希望你能提供一些关于实际应该生成什么代码的提示,如果不是在这个文件中,可能在下一个,也许是在前一个。链接器会知道。编译器只记得“如果我找到一个函数体模板,我会用T = string为它生成代码”。但不幸的是,它永远不会有机会。
链接器然后运行,替换call to constructor(int)
标记,搜索它是否找到它的实现,在terminallog.cc.o中找到它。
然后它尝试查找从未生成过的Terminallog::operator<<(string)
。不是在这里,不是在terminallog.cc.o,没有,并且悲惨地失败。
编译器仅在看到函数体时才为模板函数生成代码。你可以尝试简单地做 Terminallog clog(3); clog&lt;&lt; “喇嘛”; 在terminallog.cc中的Terminallog的构造函数中。 当编译器看到它时,它将生成Terminallog :: operator&lt;&lt;(string),链接器将在编译的terminallog.cc.o目标文件中找到它。
这应该告诉您,您必须将头部提供给头部中的函数,以便可以将其转储到使用它的每个.cc文件中,并且编译器可以动态生成它所需的内容。 否则你必须猜测T可以传入什么,这比略微膨胀的头文件更令人讨厌。
(我在这里写的内容有一些简化和不准确之处,但它的要点应该是合理的。)