0

The following excerpt is from pages 686-687 of Programming: Principles and Practice by Bjarne Stroustrup:

19.3.4 Containers and inheritance

There is one kind of combination of object-oriented programming and generic programming that people always try, but it doesn’t work: attempting to use a container of objects of a derived class as a container of objects of a base class. For example:

vector<Shape> vs;
vector<Circle> vc;

vs = vc;                // error: vector<Shape> required

void f(vector<Shape>&);
f(vc);                  // error: vector<Shape> required

But why not? After all, you say, I can convert a Circle to a Shape! Actually, no, you can’t. You can convert a Circle* to a Shape* and a Circle& to a Shape&, but we deliberately disabled assignment of Shapes, so that you wouldn’t have to wonder what would happen if you put a Circle with a radius into a Shape variable that doesn’t have a radius (§14.2.4). What would have happened — had we allowed it — would have been what is called “slicing” and is the class object equivalent of integer truncation (§3.9.2).

I'm not sure I understand why vs = vc is an error. The annotation says // error: vector<Shape> required. I tried the following toy example:

#include "../std_lib_facilities.h"

class B {
    
};

class D : public B {
    int x;

public:
    D() {
        x = 7;
    }
};

class D2 : B {
    
};

// vector<D>& vector<D>::vector<B>() {
//     return vector<B>;
// }

int main() {

    vector<B> vb;
    vector<D> vd;
    
    vb = vd;

    return 0;

}

When I attempt to compile it, the error message I get reads:

test.cpp:35:11: error: no match for 'operator=' (operand types are 'Vector' and Vector<D>)
35 | vd2 = vd;
| ^~
In file included from test.cpp:1:
../std_lib_facilities.h:70:27: note: candidate: 'Vector& Vector::operator=(const Vector&)'
70 | template< class T> struct Vector : public std::vector
| ^~~~~~
../std_lib_facilities.h:70:27: note: no known conversion for argument 1 from 'Vector' to 'const Vector&'
../std_lib_facilities.h:70:27: note: candidate: 'Vector& Vector::operator=(Vector&&)'
../std_lib_facilities.h:70:27: note: no known conversion for argument 1 from 'Vector' to 'Vector&&'

So, it seems that it's not so much that a vector<Shape> is required, but rather that vector<Shape> does not have an assignment operator that takes a vector<Circle> as input, and vector<Circle> does not have the conversion function vector<Shape>(). Is this correct?

This also got me thinking about whether it would be possible to supply a conversion function for the vector<D> specialisation myself, but when I try to define vector<D>& vector<D>::vector<B>(), I get the following error:

test.cpp:22:10: error: specializing member 'Vector::operator=' requires 'template<>' syntax 22 | vector& vector::vector() {
| ^

I'm not sure how to write it in template syntax, or if it is even possible to do so. I don't quite see the problem with being able to augment a specialisation with our own member functions. In the last paragraph of the section, Stroustrup says:

Inheritance is a powerful and subtle mechanism and templates do not implicitly extend its reach. [..] Just remember that "D is a B" does not imply "C<D> is a C<B>" for an arbitrary template C.

So, "D is a B" does not imply "C<D> is a C<B>", but that doesn't mean that both can't be true at the same time. So, why wasn't I able to define the conversion function?

user51462
  • 1,658
  • 2
  • 13
  • 41
  • you cannot provide a conversion for `std::vector` in a portable way. – 463035818_is_not_an_ai May 24 '23 at 13:27
  • just wording, but there is no contradiction between "a vector is required" and "vector does not have an assignment operator that takes a vector as input, and vector does not have the conversion function vector()". The latter is the reason for the former being true. " it's not so much..." -> it is very much about that – 463035818_is_not_an_ai May 24 '23 at 13:30
  • You can provide your own slicing conversion function. Slicing conversion functions are of limited usefulness (in general). They may be useful in rare cases in a specific context. Or for example, change `vb = vd;` to `vb = vector(vd.begin(), vd.end());` which is basically what you'd be doing in a slicing conversion function. – Eljay May 24 '23 at 13:41
  • 1
    @Eljay: You don't even need to move from a temporary `vector`, you can do it in-place using `.assign(begin_iterator, end_iterator)` – Ben Voigt May 24 '23 at 14:39
  • 1
    Thank you 463035818_is_not_a_number and Ted Lyngmo for your answers, I have upvoted both. – user51462 May 25 '23 at 00:18

2 Answers2

2

In my humble opinion it is unfortunate that the book conflates two issues. They are very much related, but each on its own would deserve an in depth explanation.

One is that std::vector<A> can't be converted to std::vector<B> even though A can be converted to B. std::vector simply does not provide the conversion.

The other issue is that when you are using inheritance then most of the time a std::vector<base> is a bad idea. For this I refer you to What is object slicing?.


Further note that the book is talking about std::vector specifically. There is nothing that prevents you to provide a conversion from foo<A> to foo<B> when A can be converted to B (see below). And the standard library does that sometimes. For example a shared_ptr<T> can be constructed from a Y* when Y* can be converted to a T* (see here https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr). Also std::pair has implicit conversions enabled, for example the following compiles:

 std::pair<int,int> x;
 std::pair<double,double> y;
 x = y;

For why this works see here: https://en.cppreference.com/w/cpp/utility/pair/pair.


std::vector does not have such conversions and you cannot easily add them. You cannot add members to std::vector neither are you allowed to add specializations for std::vector<my_type>. Consider that implicit conversions often do more harm than good, because they potentially hide bugs:

void foo(const std::vector<A>&);
void bar(const std::vector<B>&);

std::vector<A> a;
bar(a);           // ups, typo, but would compile if conversions would be available

It is common to make conversions explicit and only enable implicit conversions when their use is natural and desired (as for example with shared_ptr its natural, because its just what raw pointers do as well).

On the other hand, nothing forbids you to write a function to make the conversion explicit:

std::vector<B> convert(const std::vector<A>&);

bar( convert(a) );  // explicit is good!

So, "D is a B" does not imply "C is a C", but that doesn't mean that both can't be true at the same time. So, why wasn't I able to define the conversion function?

You should not define the conversion for std::vector. You can do it for your own template:

struct A {};
struct B {
    operator A() const { return {}; }
};

template <typename T>
struct foo {
    T t;

    template <typename U>
    operator foo<U>() const {
        return {static_cast<U>(t)};
    }
};

int main() {
    foo<A> fa;
    foo<B> fb;
    fa = fb;   // uses implicit conversion 
    //fb = fa; // error 
}

Live Demo

However, this is just a contrived example. In reality you almost always want the conversion operators to be explicit.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • Thank you, @463035818_is_not_a_number. You mention: "...neither are you allowed to add specializations for `std::vector`." I'm not sure what this means. If we can create a template for `std::vector` (as is shown in the first code snippet of Ted's answer), then wouldn't instantiating that template create a specialisation for `std::vector`? – user51462 May 25 '23 at 00:48
  • @user51462 we are not sure if it is allowed what Ted shows in the answer. However, the thing about specializing `std::vector` in the answer is only for illustration. The crucial point is that it would reuquire a lot of work, it requires so much work that whether it is allowed or not is kind of secondary. See also here: https://stackoverflow.com/questions/74880400/std-class-specialization-meeting-the-standard-library-requirements-for-the-ori – 463035818_is_not_an_ai May 25 '23 at 07:40
  • I see, I figured that since it compiled, it would be ok. But I'm guessing there's more to it that I don't fully understand right now. Thank you @463035818_is_not_a_number. – user51462 May 25 '23 at 23:38
2

Is this correct?

Yes, the two distinct types vector<Shape> and vector<Circle> can't be implicitly converted into eachother.

So, why wasn't I able to define the conversion function?

You didn't show us what you tried, only the error.

In order to specialize std::vector<D> you'd need something like this:

template<>
class std::vector<D> {
public:
    std::vector<D>& operator=(const std::vector<B>&) {
        return *this;
    }
    // all the rest of the gory std::vector implementation details
};

But specializing std::vector may not be allowed and would require a lot of work so that's not going to fly.


You could inherit from std::vector<D> and add your conversion function:

class vectorD : public std::vector<D> {
public:
    using std::vector<D>::vector;
    using std::vector<D>::operator=;

    vectorD& operator=(const std::vector<B>&) {
        return *this;
    }
};

Note that inherting from std:: classes is often frowned upon but there's nothing preventing you to do it and as long as you don't pass base class pointers to vector<D> around and expect to be able to delete a vectorD thorugh one of those, it's ususally fine.


A third option would be to define how to convert from B to D in D itself. This makes sense in most situations:

class D : public B {
    int x = 7;

public:
    D() = default;
    D(const B&) {}      // define how to convert a B into a D
};

You can then simply use the assign member function of the vector:

vd.assign(vb.begin(), vb.end());
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • afaik adding specializations to standard library templates is not allowed (unless explicitly stated, like eg for `std::hash`) – 463035818_is_not_an_ai May 24 '23 at 14:35
  • didnt find some relevant reference for `std::vector`. Though even if not allowed, imho it is worth to show, its not that tempting to actually do it anyhow – 463035818_is_not_an_ai May 24 '23 at 14:36
  • 1
    @463035818_is_not_a_number You are correct and I meant to write that but it slipped my mind. Added that now. – Ted Lyngmo May 24 '23 at 14:37
  • @463035818_is_not_a_number I just realized that I've actually asked if it's allowed myself but that question is still unanswered. :-) I'm thinking that it actually is allowed because the standard says: _"Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespace `std` provided that..."_ - but I've left it open for the more language-lawyer savvy to answer. – Ted Lyngmo May 24 '23 at 16:39
  • do you have a link to the question? – 463035818_is_not_an_ai May 24 '23 at 16:42
  • 1
    @463035818_is_not_a_number Yeah, I put that in the answer too. It's about `std::array` but I suspect the same goes for `std::vector`. – Ted Lyngmo May 24 '23 at 16:43