使用不同的实现文件来实现多态是不是可以?
Is it okay to use different implementation files to achieve polymorphism?
如果给定接口有多个所需的实现,但在编译时之前已知所需的具体实现,那么简单地将 make 文件定向到同一头文件的不同实现文件是错误的吗?
例如,如果有一个定义汽车的程序(Car.h)
// Car.h
class Car {
public:
string WhatCarAmI();
}
并且在构建时我们知道我们想要它是法拉利还是菲亚特,给每一个相应的文件:
// Ferrari.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Ferrari"; }
而对于另一种情况(不出所料)
// Fiat.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Fiat"; }
现在,我知道我可以制作 Car 的 Fiat 和 Ferrari 派生对象,并在运行时选择我想要构建的对象。同样,我可以将其模板化并让编译器在编译时选择要构建的内容。但是,在这种情况下,这两个实现都引用了不应该相交的单独项目。
鉴于此,按照我的建议简单地 select 给定项目的 makefile 中的正确 .cpp 是否错误?最好的方法是什么?
实施
因为这是静态多态性,奇怪的重复模板模式可能比交换 cpp 文件更惯用——这看起来很老套。如果您想让多个实现在一个项目中共存,同时又易于与强制 single-implementation 构建系统一起使用,那么 CRTP 似乎是必需的。我会说它的 well-documented 性质和兼顾两者的能力(因为你永远不知道以后需要什么)给了它优势。
简而言之,CRTP 看起来有点像这样:
template<typename T_Derived>
class Car {
public:
std::string getName() const
{
// compile-time cast to derived - trivially inlined
return static_cast<T_Derived const *>(this)->getName();
}
// and same for other functions...
int getResult()
{
return static_cast<T_Derived *>(this)->getResult();
}
void playSoundEffect()
{
static_cast<T_Derived *>(this)->playSoundEffect();
}
};
class Fiat: public Car<Fiat> {
public:
// Shadow the base's function, which calls this:
std::string getName() const
{
return "Fiat";
}
int getResult()
{
// Do cool stuff in your car
return 42;
}
void playSoundEffect()
{
std::cout << "varooooooom" << std::endl;
}
};
(我之前已经在派生的实现函数前加上 d_
,但我不确定这有什么好处;事实上,它可能会增加歧义...)
要了解 CRTP 中真正发生的事情 - 一旦掌握就很简单! - 周围有很多指南。您可能会发现很多变体,然后选择您最喜欢的那个。
Compile-time 选择实现
回到另一方面,如果您确实想限制在 compile-time 的实现之一,那么您可以使用一些预处理器宏来强制执行派生类型,例如:
g++ -DMY_CAR_TYPE=Fiat
以后
// #include "see_below.hpp"
#include <iostream>
int main(int, char**)
{
Car<MY_CAR_TYPE> myCar;
// Do stuff with your car
std::cout << myCar.getName();
myCar.playSoundEffect();
return myCar.getResult();
}
您可以在单个 header 和 #include
中声明所有 Car 变体,或者使用类似于这些线程中讨论的方法 - Generate include file name in a macro / Dynamic #include based on macro definition - 来生成 #include
来自同一个 -D
宏。
在编译时选择 .cpp 文件是可以的并且完全合理... 如果 被忽略的 .cpp 文件将不会编译。这是选择平台特定实现的一种方法。
但一般来说 - 如果可能(例如在你的简单示例中) - 最好使用模板来实现静态多态性。如果您需要在编译时做出选择,请使用预处理器宏。
如果这两个实现 指的是不应该交叉的独立项目 但仍然是给定接口的 实现 ,我建议将该接口提取为单独的 "project"。这样,单独的项目就不会彼此直接相关,即使它们都依赖于提供接口的第三个项目。
在您的用例中,我认为最好使用 ifdef-blocks。这将在编译前检查!这种方法有时也用于区分同一代码的不同平台。
// Car.cpp
#include "Car.h"
#define FERRARI
//#define FIAT
#ifdef FERRARI
string Car::WhatCarAmI() { return "Ferrari"; }
#endif
#ifdef FIAT
string Car::WhatCarAmI() { return "Fiat"; }
#endif
在这些代码中,编译器将忽略法币的 ifdef-block,因为只定义了 FERRARI。这样您仍然可以对两辆车使用您想要的方法。您想要不同的一切,您可以放入 ifdefs 并简单地换出定义。
Actually instead of swapping out the defines, you'd leave your code alone and
provide the definitions on the GCC command line using the -D
build switch,
depending on what build configuration were selected.
如果给定接口有多个所需的实现,但在编译时之前已知所需的具体实现,那么简单地将 make 文件定向到同一头文件的不同实现文件是错误的吗?
例如,如果有一个定义汽车的程序(Car.h)
// Car.h
class Car {
public:
string WhatCarAmI();
}
并且在构建时我们知道我们想要它是法拉利还是菲亚特,给每一个相应的文件:
// Ferrari.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Ferrari"; }
而对于另一种情况(不出所料)
// Fiat.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Fiat"; }
现在,我知道我可以制作 Car 的 Fiat 和 Ferrari 派生对象,并在运行时选择我想要构建的对象。同样,我可以将其模板化并让编译器在编译时选择要构建的内容。但是,在这种情况下,这两个实现都引用了不应该相交的单独项目。
鉴于此,按照我的建议简单地 select 给定项目的 makefile 中的正确 .cpp 是否错误?最好的方法是什么?
实施
因为这是静态多态性,奇怪的重复模板模式可能比交换 cpp 文件更惯用——这看起来很老套。如果您想让多个实现在一个项目中共存,同时又易于与强制 single-implementation 构建系统一起使用,那么 CRTP 似乎是必需的。我会说它的 well-documented 性质和兼顾两者的能力(因为你永远不知道以后需要什么)给了它优势。
简而言之,CRTP 看起来有点像这样:
template<typename T_Derived>
class Car {
public:
std::string getName() const
{
// compile-time cast to derived - trivially inlined
return static_cast<T_Derived const *>(this)->getName();
}
// and same for other functions...
int getResult()
{
return static_cast<T_Derived *>(this)->getResult();
}
void playSoundEffect()
{
static_cast<T_Derived *>(this)->playSoundEffect();
}
};
class Fiat: public Car<Fiat> {
public:
// Shadow the base's function, which calls this:
std::string getName() const
{
return "Fiat";
}
int getResult()
{
// Do cool stuff in your car
return 42;
}
void playSoundEffect()
{
std::cout << "varooooooom" << std::endl;
}
};
(我之前已经在派生的实现函数前加上 d_
,但我不确定这有什么好处;事实上,它可能会增加歧义...)
要了解 CRTP 中真正发生的事情 - 一旦掌握就很简单! - 周围有很多指南。您可能会发现很多变体,然后选择您最喜欢的那个。
Compile-time 选择实现
回到另一方面,如果您确实想限制在 compile-time 的实现之一,那么您可以使用一些预处理器宏来强制执行派生类型,例如:
g++ -DMY_CAR_TYPE=Fiat
以后
// #include "see_below.hpp"
#include <iostream>
int main(int, char**)
{
Car<MY_CAR_TYPE> myCar;
// Do stuff with your car
std::cout << myCar.getName();
myCar.playSoundEffect();
return myCar.getResult();
}
您可以在单个 header 和 #include
中声明所有 Car 变体,或者使用类似于这些线程中讨论的方法 - Generate include file name in a macro / Dynamic #include based on macro definition - 来生成 #include
来自同一个 -D
宏。
在编译时选择 .cpp 文件是可以的并且完全合理... 如果 被忽略的 .cpp 文件将不会编译。这是选择平台特定实现的一种方法。
但一般来说 - 如果可能(例如在你的简单示例中) - 最好使用模板来实现静态多态性。如果您需要在编译时做出选择,请使用预处理器宏。
如果这两个实现 指的是不应该交叉的独立项目 但仍然是给定接口的 实现 ,我建议将该接口提取为单独的 "project"。这样,单独的项目就不会彼此直接相关,即使它们都依赖于提供接口的第三个项目。
在您的用例中,我认为最好使用 ifdef-blocks。这将在编译前检查!这种方法有时也用于区分同一代码的不同平台。
// Car.cpp
#include "Car.h"
#define FERRARI
//#define FIAT
#ifdef FERRARI
string Car::WhatCarAmI() { return "Ferrari"; }
#endif
#ifdef FIAT
string Car::WhatCarAmI() { return "Fiat"; }
#endif
在这些代码中,编译器将忽略法币的 ifdef-block,因为只定义了 FERRARI。这样您仍然可以对两辆车使用您想要的方法。您想要不同的一切,您可以放入 ifdefs 并简单地换出定义。
Actually instead of swapping out the defines, you'd leave your code alone and provide the definitions on the GCC command line using the
-D
build switch, depending on what build configuration were selected.