5

Consider the following piece of code

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

struct Base {
    int x;

    Base(int x) : x(x) {}
};

struct Derived : public Base {
    int y, z;

    Derived(int x) : Base(x), y(x + 1), z(x + 2) {}
};

void update(const std::vector<std::shared_ptr<const Base>>& elements) {
    for (const auto elem : elements) {
        std::cout << elem->x << "\n";
    }
}

int main(int, char**) {
    std::vector<std::shared_ptr<Derived>> elements(4);

    {
        int ctr = 0;
        std::generate(begin(elements), end(elements), [&ctr]() { return std::make_shared<Derived>(++ctr); });
    }

//    update(elements); // note: candidate function not viable: no known conversion from 'vector<shared_ptr<Derived>>' to 'const vector<shared_ptr<const Base>>' for 1st argument
    update(reinterpret_cast<std::vector<std::shared_ptr<const Base>>&>(elements));  // ok

    return 0;
}

My question is if using reinterpret_cast to cast from std::vector<std::shared_ptr<Derived>> to std::vector<std::shared_ptr<const Base>>& is feasible and accepted by standard.

I have compiled the code with clang-3.8 and gcc-6.1 with -fsanitize=undefined and it looks like it's ok. However, I seem unable to find a proper explanation on cppreference.

Of course I can easily create an appriopriate function but it's longer than one-line reinterpret_cast and requires a temporary vector.

void update(const std::vector<std::shared_ptr<Derived>>& elements) {
    std::vector<std::shared_ptr<const Base>> casted(elements.size());
    std::copy(begin(elements), end(elements), begin(casted));
    update(casted);
}
Rafal
  • 91
  • 1
  • 7
  • 2
    Is your code a real use case, or is it just an example? If it's a real use case, IMHO it's a bad use of reinterpret_cast. If it's an example, please provide a real use case of what you want to achieve – rocambille Jul 23 '16 at 07:25
  • @wasthishelpful more or less real use case; feeding a database fake object during unit tests – Rafal Jul 23 '16 at 15:55
  • In your example, you could have just used a vector of Base in your main function, or use templates in your update function. I guess it's not possible in the real case, but from the code you posted I can't see why :) – rocambille Jul 23 '16 at 17:02

2 Answers2

3

Templates in general and containers (I treat shared_ptr as a special form of container) are not covariant in C++. This means that if you have two types Base and Derived < Base and a template<typename T> class X {};, X<Base> and X<Derived> are two completely different things and not in any form of relationship.

In your case, you have an object of type std::vector<std::shared_ptr<Derived>> and then create a std::vector<std::shared_ptr<const Base>>& which is then used to access it. I think this has two issues:

  1. You cast an object of type vector into a reference type. I really wonder why this works.
  2. You access the object through a reference of unrelated different type. I think this violates the strict aliasing rule and is thus undefined behavior.

If you compile your code with gcc -fstrict-aliasing, the compiler will assume that your program is compliant with the rule and optimize it. It will generate a warning:

> Start prog.cc: In function 'int main(int, char**)': prog.cc:33:80:
> warning: dereferencing type-punned pointer will break strict-aliasing
> rules [-Wstrict-aliasing]
>      update(reinterpret_cast<std::vector<std::shared_ptr<const Base>>&>(elements));  // ok
Jens
  • 9,058
  • 2
  • 26
  • 43
  • Good point with strict aliasing, clang-3.8 accepts the code though. – Rafal Jul 23 '16 at 16:00
  • 2
    @Rafal That may be, but is still undefined behavior. Gcc also accepts it as long as you don't compile with `-fstrict-aliasing`. – Jens Jul 23 '16 at 16:51
  • 2
    @Rafal Accepting the code is also ok if it undefined behavior. It can just do anything. – Jens Jul 23 '16 at 21:38
2

reinterpret_casting like this is undefined behaviour. The code will break when casting from Derived* to Base* requires pointer adjustment. This will most likely happen when Derived uses multiple inheritance and Base is not its first base class.

struct Derived : public X, public Base { ... };

Derived* d = new Derived;

Base* b = d; // this is no longer a "no-op", since the Base sub-object
             // of Derived is not at offset 0:
             //
             // d  b
             // |  |
             // v  v
             // [Derived]
             // [X][Base]

If your goal is to just make it work in the most concise way rather than avoid the conversion through a temporary vector, then in this particular case you can use the container_cast utility proposed in this answer.

update(container_cast(elements));
Community
  • 1
  • 1
Leon
  • 31,443
  • 4
  • 72
  • 97
  • I do know about breaking when pointer adjustment is needed. This is not the case in this particular situation though. Nice idea with `container_cast`, I completely missed range constructors. – Rafal Jul 23 '16 at 15:57