简体   繁体   中英

Is it okay to use different implementation files to achieve polymorphism?

In the case where there are multiple desired implementations for a given interface, but where the specific implementation desired is known before compile time, is it wrong simply to direct the make file to different implementation files for the same header?

For example, if have a program defining a car (Car.h)

// Car.h
class Car {
  public: 
    string WhatCarAmI();
}

and at build time we know whether we want it to be a Ferrari or a Fiat, to give each either of the corresponding files:

// Ferrari.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Ferrari"; }

whilst for the other case (unsurprisingly)

// Fiat.cpp
#include "Car.h"
string Car::WhatCarAmI() { return "Fiat"; }

Now, I am aware that I could make both Fiat and Ferrari derived objects of Car and at runtime pick which I would like to build. Similarly, I could templatize it and make the compiler pick at compile time which to build. However, in this case the two implementations both refer to separate projects which should never intersect.

Given that, is it wrong to do what I propose and simply to select the correct .cpp in the makefile for the given project? What is the best way to do this?

Implementation

As this is static polymorphism, the Curiously Recurring Template Pattern is probably vastly more idiomatic than swapping a cpp file - which seems pretty hacky. CRTP seems to be required if you want to let multiple implementations coexist within one project, while being easy to use with an enforced single-implementation build system. I'd say its well-documented nature and ability to do both (since you never know what you'll need later) give it the edge.

In brief, CRTP looks a little like this:

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;
    }
};

(I've previously prefixed derived implementation functions with d_ , but I'm not sure this gains anything; in fact, it probably increases ambiguity...)

To understand what's really going on in the CRTP - it's simple once you get it! - there are plenty of guides around. You'll probably find many variations on this, and pick the one you like best.

Compile-time selection of implementation

To get back to the other aspect, if you do want to restrict to one of the implementations at compile-time, then you could use some preprocessor macro(s) to enforce the derived type, eg:

g++ -DMY_CAR_TYPE=Fiat

and later

// #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();
}

You could either declare all Car variants in a single header and #include that, or use something like the methods discussed in these threads - Generate include file name in a macro / Dynamic #include based on macro definition - to generate the #include from the same -D macro.

Choosing a .cpp file at compile time is OK and perfectly reasonable... if the ignored .cpp file would not compile. This is one way to choose a platform specific implementation.

But in general - when possible (such as in your trivial example case) - it's better to use templates to achieve static polymorphism. If you need to make a choice at compile time, use a preprocessor macro.

If the two implementations refer to separate projects which should never intersect but still are implementations for a given interface , I would recommend to extract that interface as a separate "project". That way the separate projects are not directly related to each other, even though they both depend on the third project which provides the interface.

In your use case I think it would be best to use ifdef -blocks. This will be checked before compilation! This method is also sometimes used to distinct between different platforms for the same code.

// 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

In these code the compiler will ignore the ifdef -block of fiat, because only FERRARI is defined. This way you can still use methods you want to have for both cars. Everything you want different, you can put in ifdefs and simply swap out the defines.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM