一次经典的 C++ 排错过程

2019, Sep 03    

最近在看 C++ Primer 英文第四版,其中有一章节讲到了模板类的成员函数该怎么写,并且给出了一个队列(Queue)的例子,源代码可以参考这里

我使用了 VSCode 插件商城里的 Create Makefile 插件生成了 Makefile,但是编译的时候却报了如下错误:

liuchushudeMBP:Queue liuchushu$ make all
g++ -Wall -Werror -Wextra -pedantic -std=c++17 -g -fsanitize=address   -c -o Queue.o Queue.cc
Queue.cc:2:6: error: variable has incomplete type 'void'
void Queue<Type>::destroy()
     ^
Queue.cc:2:11: error: expected ';' at end of declaration
void Queue<Type>::destroy()
          ^
          ;
Queue.cc:2:11: error: expected unqualified-id
Queue.cc:10:17: error: qualified name refers into a specialization of variable template 'Queue'
void Queue<Type>::pop()
     ~~~~~~~~~~~^
Queue.cc:2:6: note: variable template 'Queue' declared here
void Queue<Type>::destroy()
     ^
4 errors generated.
make: *** [Queue.o] Error 1

这个报错简直让人云里雾里,百思不得其解。首先,解释一下我在 Queue.h 文件里的写法:

#ifndef QUEUE_H
#define QUEUE_H

// other code...

#include "Queue.cc"
#endif

#include "Queue.cc" 这一行的目的是将 .cc 文件里的函数定义包含进来,这种写法在 C++ 里被称为 inclusion compilation model,暂且将其翻译为包含编译模式。使用这种写法是为了让 C++ 的模板方法在编译时,编译器既可以看到模板方法的声明又可以看到其定义,同时声明放在 .h 文件里、定义放在 .cc 文件里,以便于区分。举个例子:

// Queue.h
template <class Type> class Queue {
    // other declarations...
    template <class Iter> void copy_elems(Iter, Iter);
    // other declarations...
};

// Queue.cc
template <class Type> template <class Iter>
void Queue<Type>::copy_elems(Iter beg, Iter end)
{
    while (beg != end) {
        push(*beg);
        ++beg;
    }
}

在上面的代码片段中,头文件 Queue.h 中的 Queue 类中声明了模板函数 copy_elems,而其定义则写在 Queue.cc 里,我们通过在 Queue.h 里包含 Queue.cc 文件,就可以让编译器同时看到模板函数 copy_elems 的声明和定义。

可为什么会有上面那个莫名其妙的错误呢?我尝试着将 #include "Queue.cc" 这一行注释掉,还是有同样的问题!

liuchushudeMBP:Queue liuchushu$ make all
g++ -Wall -Werror -Wextra -pedantic -std=c++17 -g -fsanitize=address   -c -o Queue.o Queue.cc
Queue.cc:2:6: error: variable has incomplete type 'void'
void Queue<Type>::destroy()
     ^
Queue.cc:2:11: error: expected ';' at end of declaration
void Queue<Type>::destroy()
# 省略其他报错

等等!我已经注释掉 #include "Queue.cc" 了,按理说不应该再报这个文件的错误了啊。哦!原来是我的 Makefile 里编译了这个 .cc 文件:

CXX = g++
CXXFLAGS = -Wall -Werror -Wextra -pedantic -std=c++17 -g -fsanitize=address
LDFLAGS =  -fsanitize=address

SRC = Queue.cc program.cc
OBJ = $(SRC:.cc=.o)
EXEC = program

all: $(EXEC)

$(EXEC): $(OBJ)
	$(CXX) $(LDFLAGS) -o $@ $(OBJ) $(LBLIBS)

clean:
	rm -rf $(OBJ) $(EXEC)

一旦编译这个 .cc 文件,而它有没有包含 Queue.h 文件,那么就找不到 Queue 的定义,就会产生上述报错。我们无需编译 Queue.cc 文件,因为 Queue.h 文件就已经包含了 .cc 文件里的函数定义了。

最后,在 Makefile 里编译 Queue.cc 的编译就可以正常编译了:

SRC = program.cc