Search code examples
c++vectorargumentsc++17abstract-class

How can I use vector of an abstract/interface class as function parameter?


I'm trying to use a vector of an abstract/interface class Base as parameter to a function g (in main.cpp), so that i can pass vectors of derived classes as arguments.

Since I can't have instances of Base, I'm trying to pass vectors of unique pointers instead -- as this seems to be the preferred way to do it. However, I'm not allowed to convert from vector<unique_ptr<Test::A>> to vector<unique_ptr<Test::Base>> for some reason and I don't understand why.

Question: why is this not allowed and how can I solve it?

module.hpp looks like this:

#ifndef _MODULE_HPP
#define _MODULE_HPP

namespace Test
{
    class Base
    {
    public:
        virtual ~Base(){};

        virtual int f(int a, int b) = 0;
    };

    class A : public Base
    {
    public:
        A();
        ~A() = default;

        int f(int a, int b);
    };
}

#endif

module.cpp looks like this:

#include <module/module.hpp>

Test::A::A() {}

int Test::A::f(int a, int b)
{
    return a + b;
};

main.cpp looks like this:

#include <module/module.hpp>

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

void g(std::vector<std::unique_ptr<Test::Base>> v)
{
    for (std::unique_ptr<Test::Base> &vi : v)
    {
        std::cout << vi->f(1, 2) << " ";
    }
    std::cout << std::endl;
}

int main(int argc, char **argv)
{
    (void)argc;
    (void)argv;

    std::vector<std::unique_ptr<Test::A>> v;

    v.emplace_back(std::make_unique<Test::A>());
    v.emplace_back(std::make_unique<Test::A>());

    g(v);

    return 0;
}

When I try to compile the code I get the following error:

g++ -std=c++17 -Wall -Wextra  -I src -I include -c -o build/module/module.o src/module/module.cpp
g++ -std=c++17 -Wall -Wextra  -I src -I include -c -o build/main.o src/main.cpp
src/main.cpp: In function ‘int main(int, char**)’:
src/main.cpp:26:4: error: could not convert ‘v’ from ‘vector<unique_ptr<Test::A>>’ to ‘vector<unique_ptr<Test::Base>>’
   26 |  g(v);
      |    ^
      |    |
      |    vector<unique_ptr<Test::A>>
make: *** [Makefile:36: build/main.o] Error 1

Solution

  • std::vector<std::unique_ptr<Base>> and std::vector<std::unique_ptr<Derived>> are not covariant, and no conversion function between them exists, so you cannot simply treat one as the other. They are completely different, unrelated types.

    Polymorphic Storage

    If you need polymorphic storage, why not just use std::vector<std::unique_ptr<Base>> everywhere? You can insert std::unique_ptr<Derived> into it like this:

    // note: passing by reference to avoid having to move the vector
    //       (copying would be illegal)
    void g(const std::vector<std::unique_ptr<Test::Base>> &v)
    {
        for (const std::unique_ptr<Test::Base> &vi : v)
        {
            std::cout << vi->f(1, 2) << " ";
        }
        std::cout << std::endl;
    }
    
    int main()
    {
        std::vector<std::unique_ptr<Test::Base>> v;
    
        v.emplace_back(std::make_unique<Test::A>());
        v.emplace_back(std::make_unique<Test::A>());
    
        g(v);
    }
    

    Accepting Vectors of Covariant Types

    Alternatively, if you just need to store a std::vector<Derived> but want to pass it into functions that take a range of Base values, you can do this:

    #include <concepts>
    
    // C++20 version using concepts
    template <std::derived_from<Test::Base> Derived>
    void g(std::vector<Derived> &v)
    {
        for (Test::Base &vi : v)
        {
            std::cout << vi.f(1, 2) << " ";
        }
        std::cout << std::endl;
    }
    
    // C++17 counterpart
    template <typename Derived>
    auto g(std::vector<Derived> &v)
        -> std::enable_if_t<std::is_base_of_v<Test::Base, Derived>>
    { ... }
    

    To call g, we need to declare std::vector<Test::A> v; in main. We could make this function even more generic if we used ranges, so we would no longer rely on std::vector specifically.

    // C++20 version
    template <std::ranges::input_range Range>
    requires (std::derived_from<std::ranges::range_value_t<Range>, Test::Base>)
    void g(Range &v)
    {
        for (Test::Base &vi : v)
        {
            std::cout << vi.f(1, 2) << " ";
        }
        std::cout << std::endl;
    }
    
    // C++17 version would be much more difficult because we lack the proper type traits.
    // If we accepted two iterators, it would become easier.
    template <typename ForwardIt>
    auto g(ForwardIt begin, ForwardIt end)
        -> std::enable_if_t<std::is_base_of_v<Test::Base, typename std::iterator_traits<ForwardIt>::value_type>>
    { ... }
    
    // call this function like g(v.begin(), v.end())
    

    Note: You could also leave the templates unconstrained, which would make this easier to implement in C++17.