今天同事遇到一个编译错误找我帮忙看一下。原以为编译错误应该好解决,没想到搞了半天才fix,在此记录一下。
由于公司代码涉及机密且比较复杂,不方便贴出来,因此我写了一个简化的例子来展示这个编译错误是如何产生的。
1.代码
假设我们有一个namespace名为zoo, 在zoo中有一个animal基类,一个name类,还有一个dog类继承自animal类。这三个类分别位于三个头文件。代码如下:
//animal.h
#pragma once
class Name;
namespace zoo {
class Animal {
virtual Name* GetName() const = 0;
};
}
//name.h
#pragma once
namespace zoo {
class Name {
};
}
//dog.h
#pragma once
#include "animal.h"
#include "name.h"
namespace zoo {
class Dog :public Animal{
public:
virtual Name* GetName() const override { return name; }
private:
Name* name;
};
}
// main.cpp
#include "dog.h"
int main()
{
return 0;
}
2. 原因分析
这段代码很简单,不需要解释。用msvc编译器来编译会得到下面的编译错误:
Error C2555 'zoo::Dog::GetName': overriding virtual function return type differs and is not covariant from 'zoo::Animal::GetName' Project2 C:\Users\vincent.zheng\source\repos\Project2\Project2\dog.h 7
第一次看这个报错信息真是看的我一脸懵,一般来讲,C2555报错是由于派生类重写基类的虚函数,但是返回类型不一致导致的,根据微软的官方文档
'class1::function1': overriding virtual function return type differs and is not covariant from 'class2::function2'
A virtual function and a derived overriding function have identical parameter lists but different return types.
可是在我们这个例子中,基类和派生类的GetName()函数返回值明明是一样的,都是Name类型。这是一个实实在在的类型,根本不存在covariant的问题。那么问题到底出在哪里呢?
一开始我认为编译器报错信息不准确,就忽略了这个报错,在代码别的地方一通瞎改。可想而知,这样搞肯定是不管用的,搞了一个多小时,这个报错始终跟个牛皮癣一样英魂不散。后来实在累了,于是起来活动一下,喝杯水,水水群再重新坐下来仔细看代码。
终于让我给看出点端倪来,我发现在animal.h文件中,用了一个class前置声明,是这样写的(当然具体的项目代码分散在各个文件,而且位置距离很远,没有这么简单):
class Name;
namespace zoo {
class Animal {
virtual Name* GetName() const = 0;
};
}
这简直是个大坑!class Name的前置声明放在了namespace zoo外面,也就是说编译在编译基类的GetName函数时会认为这里Name是一个全局的class,而不是在namespace zoo里面定义的那个Name。而在文件dog.h中包含了name.h头文件,因此dog类的getName函数返回值是zoo::Name类型。基类和派生类的函数签名分别为
Name* zoo::Animal::GetName()const;
zoo::Name zoo::Dog::GetName() const;
显然这两者的返回值是不一样的,因此会报C2555错误:
Error C2555 'zoo::Dog::GetName': overriding virtual function return type differs and is not covariant from 'zoo::Animal::GetName' Project2 C:\Users\vincent.zheng\source\repos\Project2\Project2\dog.h 7
3. 解决方法
知道了原因之后,解决办法自然而然也就有了。要么我们animal.h中的前置声明写在namespace zoo里面,如:
#pragma once
namespace zoo {
class Name;
class Animal {
virtual Name* GetName() const = 0;
};
}
要么,我们不用前置声明,直接include 头文件
#pragma once
#include "Name.h"
namespace zoo {
class Animal {
virtual Name* GetName() const = 0;
};
}
4. 总结
通过这个例子来看编译器的报错还是相当靠谱的,可惜的是这个报错信息只把函数名打印出来了,并没有打印函数签名。如果把函数签名也打印出来,我想这个错误应该分分钟就可以被解决掉。对比下g++编译器的报错信息,明显比MSVC提供的信息要多一些。
In file included from main.cpp:1:
./dog.h:7:17: error: return type of virtual function 'GetName' is not covariant with the return type of the function it overrides ('zoo::Name *' is not derived from 'Name *')
virtual Name* GetName() const override { return name; }
~~~~~ ^
./animal.h:5:17: note: overridden virtual function is here
virtual Name* GetName() const = 0;
~~~~~ ^
1 error generated.
当然,说来说去还是自己经验不足,技术差不能怪编译器:)。这个问题也给了我一个教训:不要轻易放过编译器的报错,大多数情况下编译器还是比我们自己靠谱的多!