0

My colleague and I are implementing Google Test for our code base and are running into a few issues with Contravariance regarding using standard template containers.

So, Google Test requires us to make a pure virtual Interface class that mirrors our actual class which will inherit the interface and implement all virtual functions. This is to be used in Google Mock for testing. This is a strict requirement to work too, otherwise we need to add templates to all of our classes which will only be of one type... this seems rather unintuitive just to get the test code working.

So we just investigated some code that exhibits the behavior of the issue:

#include <vector>
#include <string>
#include <iostream>

class Fruit{
public:
    Fruit(std::string colorIn) : color(colorIn) {}
    std::string color;
};

class Apple : public Fruit{
public:
    Apple() :  Fruit("Red"){ appleType = "Honey Crisp"; }
    Apple(const Fruit& fruit) : Fruit(fruit.color) { appleType = "Honey Crisp"; }
    std::string appleType;
};

class Banana : public Fruit{
public:
    Banana() :  Fruit("Yellow"){ bananaType = "Dole"; }
    Banana(const Fruit& fruit) : Fruit(fruit.color) { bananaType = "Dole"; }
    std::string bananaType;
};

void takeMyFruit(std::vector<Fruit>& fruits){
    if(!fruits.empty()){
        std::cout << "Take my " << fruits[0].color << " " << ((Banana)(fruits[0])).bananaType << " banana." << std::endl;
        std::cout << "Take my " << fruits[1].color << " " << ((Apple)(fruits[1])).appleType << " apple." << std::endl;
    }else{
        std::cout << "You gave me an empty bag?" << std::endl;
    }
}

int main(){
    std::vector<Fruit> fruits;
    fruits.push_back(Banana());
    fruits.push_back(Apple());

    std::vector<Banana> bananas = { Banana() };

    std::vector<Apple> apples = { Apple() };

    takeMyFruit(fruits);    //Why can I do this?
    //takeMyFruit(bananas); //Compile error due to contravariance
    //takeMyFruit(apples);  //Compile error due to contravariance

    return 0;
}

We need to be able to compile something that can take a base type for a container, i.e. std::vector<BaseType>, but we are only populating it with one single DerivedType.

Why would we be allowed to mix two different derived types in the std::vector<Fruit> in the above code example we created, (i.e. Apple and Banana), but not be able to pass a std::vector<DerivedType> to a functional parameter that accepts std::vector<BaseType>?

What would be the best way to get around this issue regarding Google Test and Google Mock. They say that if production code is being changed to suit the need for tests, then it probably isn't the best practice.

Another way to do this that we saw was to add templates for the derived types to any class that defines them as members. Doing this would be a rather large rehaul and would then require any user of the library we're creating to have to wrap every instantiation of these new classes that hold these interface/derived types just to get Google Mock to work.

We're operating on legacy code at this point that can't be changed this much to incorporate Google Mock. We can't just skip testing these new class types either, what would be the best method for moving forward?

David G
  • 94,763
  • 41
  • 167
  • 253
Blanky
  • 63
  • 9
  • 2
    I think you should use vector fruits, instead of vector or vector> will be a better choice – Tony May 09 '19 at 16:30
  • 2
    At least it's protecting you from [slicing](https://stackoverflow.com/q/274626/10077). – Fred Larson May 09 '19 at 16:31
  • @Tony In our actual code base we do use pointers/smart pointers as the template type of our containers. The same thing happens. – Blanky May 09 '19 at 16:45
  • @FredLarson We only use the functionality that is common with the BaseType for all DerivedTypes when we use them in these new classes that defined them as members. It's just to hold on to them, our driving interface knows everything's dynamic and static type and so no slicing occurs there. (i.e. the BaseType has a setPosition function, and so do both type of DerivedTypes that can be stored as members inside of the new class). And we can't just store the DerivedType as the BaseType as a member, because when they request that object, the driver does the calls which will be valid. – Blanky May 09 '19 at 16:48

1 Answers1

0

Please see the code below.

#include <vector>
#include <string>
#include <iostream>
#include <memory>

using namespace std;

class Fruit
{
    public:
    Fruit(std::string colorIn) : color(colorIn) { }
    std::string color;
};

class Apple : public Fruit{
public:
    Apple() :  Fruit("Red") { appleType = "Honey Crisp"; }
    Apple(const Fruit& fruit) : Fruit(fruit.color) { appleType = "Honey Crisp"; }
std::string appleType;
};

class Banana : public Fruit{
public:
    Banana() :  Fruit("Yellow") { bananaType = "Dole"; }
    Banana(const Fruit& fruit) : Fruit(fruit.color) { bananaType = "Dole"; }
std::string bananaType;
};

void takeMyFruit(std::vector<shared_ptr<Fruit>>& fruits)
{
    if (!fruits.empty())
    {
        for (vector<shared_ptr<Fruit>>::const_iterator it = fruits.begin(); it != fruits.end(); ++it)
            std::cout << "Take my " << (*it)->color;
        // You shouldn't use the following two lines as you don't know what is in the vector.
        //        std::cout << "Take my " << fruits[0]->color << " " << std::dynamic_pointer_cast<Banana>(fruits[0])->bananaType << " banana." << std::endl;
        //        std::cout << "Take my " << fruits[1]->color << " " << std::dynamic_pointer_cast<Apple>(fruits[1])->appleType << " apple." << std::endl;
    }
    else
    {
        std::cout << "You gave me an empty bag?" << std::endl;
    }
}

int main()
{
    std::vector<std::shared_ptr<Fruit>> fruits;
    fruits.push_back(std::make_shared<Banana>());
    fruits.push_back(std::make_shared<Apple>());

    std::vector<std::shared_ptr<Fruit>> bananas = { std::make_shared<Banana>() };

    std::vector<std::shared_ptr<Fruit>> apples = { std::make_shared<Apple>() };

    takeMyFruit(fruits);    //Why can I do this?
    takeMyFruit(bananas); //OK now
    takeMyFruit(apples);  //OK now

    return 0;
}

If you want to have a functionality to show the fruit type, the correct way is to add a virtual function in Fruit

virtual string FruitDetailType() = 0;

and implement it in the derived class - lets say, apple class

virtual string FruitDetailType()
{
    return "Apple, Honey Crisp";
}
//or,
virtual string FruitDetailType()
{
    return appleType;
}
Tony
  • 632
  • 1
  • 4
  • 18