Solon
Solon

Reputation: 362

c++ inheritance and container covariance

I am trying to build the following architecture:

struct A{
   int x;
}
struct B: public A{
   int additinal_data;
}
struct ContainerA{
  std::vector<A> va; 
}
struct ContianerB{
  std::vector<B> vb;
  int additional_data;
}

I want to reorganize the following code in a way I would be able to call a function not only for ContainerA type but also for ContainerB.

double sum(ContainerA &ca) {
   double res = 0;
   for (const auto &a: ca.va) 
       res += a.x;
   return res;
}

It would also be nice to have an ability to move a simular function as a member of ContainerA but be accessible from ContainerB as well (i.e ContainerA ca; ca.sum()) However, ContinerB is not covariant to ContainerA and I struggle to make it so. How can I rework my architecture to allow such a call?

Upvotes: 1

Views: 208

Answers (4)

alfC
alfC

Reputation: 16242

Consider using static polymorphism (templates).

(code not tested)

auto const& inner_vector(ContainerA& ca){return ca.va;}
auto const& inner_vector(ContainerB& cb){return cb.vb;}

template<class TContainer>
double sum(TContainer &c){
   double res = 0;
   for (const auto &a: inner_vector(c)) 
       res += a.x;
   return res;
}

Upvotes: 0

user12927872
user12927872

Reputation:

I think it's impossible to static_cast a Derived reference of a value to it's Base type reference in a getIdentifer method as shown implicitly in Steve Jessop's covariance example, because then you'd be creating a "temporary reference", but I might be wrong. So instead I made a method (called getElement) that returns a possibly casted pointer, which will definitely exist.

Because you might really want to have the explicit relation between ContainerA and ContainerB (in case of duplicated code), I wrote a class-based example on how you could implement it. Else, using the templated function is probably the way to go.

If you are using structs, you don't need the public, protected and private labels, nor setters for the variables, but you of course do need some getIdentifier method to access B type elements.

Here is a demonstrating program:

A

#pragma once
#include <iostream>

class A
{
public:
    A(const int& data);

    friend std::ostream& operator<<(std::ostream& s, const A& a);
protected:
    virtual std::ostream& toStream(std::ostream& s) const;
private:
    int m_data;
};

A::A(const int& data)
    : m_data{ data }
{
}

std::ostream& A::toStream(std::ostream& s) const
{
    return s << m_data;
}

std::ostream& operator<<(std::ostream& s, const A& a)
{
    return a.toStream(s);
}

B

#pragma once
#include "A.h"

class B : public A
{
public:
    B(const int& data, const int& additionalData);
    void setAdditionalData(int additionalData);
protected:
    virtual std::ostream& toStream(std::ostream& s) const;
private:
    int m_additinalData;
};

B::B(const int& data, const int& additionalData)
    : A{ data },
    m_additinalData{ additionalData }
{
}

void B::setAdditionalData(int additionalData)
{
    m_additinalData = additionalData;
}

std::ostream& B::toStream(std::ostream& s) const
{
    A::toStream(s);
    return s << '\t' << m_additinalData;
}

ContainerA

#pragma once
#include "A.h"

#include <vector>

class ContainerA
{
public:
    void push(A* a);
    size_t getSize() const;
    virtual A* getElement(const int& index);
    virtual A* operator[](const int& index);
protected:
    std::vector<A*> m_As;
};

void ContainerA::push(A* a)
{
    m_As.push_back(a);
}

A* ContainerA::getElement(const int& index)
{
    return m_As[index];
}

A* ContainerA::operator[](const int& index)
{
    return m_As[index];
}

size_t ContainerA::getSize() const
{
    return m_As.size();
}

ContainerB

#pragma once
#include "ContainerA.h"
#include "B.h" // The compiler should be able to tell that B is a subclass of A

class ContainerB : public ContainerA
{
public:
    B* getElement(const int& index) override;
    B* operator[](const int& index) override;
private:
    int additional_data;
};

B* ContainerB::getElement(const int& index)
{
    return static_cast<B*>(m_As[index]);
}

B* ContainerB::operator[](const int& index)
{
    return static_cast<B*>(m_As[index]);
}

main.cpp

#include "ContainerB.h"

int main()
{
    B b1{ 4, -1 };
    B b2{ 5, -1 };
    B b3{ 6, -1 };

    ContainerB contB{};
    contB.push(&b1);
    contB.push(&b2);
    contB.push(&b3);

    B* b{ contB.getElement(0) };
    b->setAdditionalData(0);

    size_t size{ contB.getSize() };
    for (int i{ 0 }; i < size; ++i) {
        std::cout << *contB.getElement(i) << std::endl;
        std::cout << *contB[i] << std::endl;
    }
}

Output

4       0
4       0
5       -1
5       -1
6       -1
6       -1

Now you can pass ContainerB to a function / method that expects a ContainerA without having to store redundant data.

Upvotes: 1

Caleth
Caleth

Reputation: 62576

how to express that ContainerB "extends" ContainerA

At the moment it doesn't. You could easily change that however:

struct ContainerA{
  std::vector<A> va; 
};
struct ContianerB{
  std::vector<B> va;
  int additional_data;
};

Static polymorphism works best when the names match.

And then

template <typename Container>
double sum(Container const& container) 
/* requires AVector<Container> */
{
   double res = 0;
   for (const auto &a: container.va) 
       res += a.x;
   return res;
}

With C++20 concepts for nicer validation

template<typename T> concept ARange = 
    std::range<T>
 && std::derived_from<T::value_type, A>;

Upvotes: 2

R Sahu
R Sahu

Reputation: 206567

One way to use sum with either a ContainerA or ContainerB.

  1. Make the function a function template.
  2. Update ContainerA and ContainerB so they can be used in a range-for loop and many of the functions in the <algorithm> header of the standard library.
struct ContainerA
{
  std::vector<A> va; 

  // Add const and non-const versions of begin() and end().

  using iterator = std::vector<A>::iterator;
  using const_iterator = std::vector<A>::const_iterator;

  iterator begin() { return va.begin(); }
  iterator end() { return va.end(); }

  const_iterator begin() const { return va.begin(); }
  const_iterator end() const { return va.end(); }

};

struct ContianerB
{
  std::vector<B> vb;
  int additional_data;

  // Add const and non-const versions of begin() and end().

  using iterator = std::vector<B>::iterator;
  using const_iterator = std::vector<B>::const_iterator;

  iterator begin() { return vb.begin(); }
  iterator end() { return vb.end(); }

  const_iterator begin() const { return vb.begin(); }
  const_iterator end() const { return vb.end(); }

};

template <typename Container>
double sum(Container const& container)
{
   double res = 0;
   for (const auto &a: container) 
       res += a.x;
   return res;
}

Upvotes: 2

Related Questions