24

The type vector<char *> is not convertible to const vector<const char*>. For example, the following gives a compilation error:

#include <vector>

using namespace std;

void fn(const vector<const char*> cvcc)
{
}

int main()
{
    vector<char *> vc = vector<char *>(); 

    fn(vc);
}

I understand why vector<char*> is not convertable to vector<const char*> - extra members of type const char * may be added to the vector, and afterwards they would be accessible as non-const. However, if the vector itself is const, this can't happen.

My best guess is that this would be harmless, but there is no way the compiler is allowed to deduce that this would be harmless.

How can this be worked around?

This question was suggested by the C++ FQA here.

Gavin Smith
  • 3,076
  • 1
  • 19
  • 25

4 Answers4

14

In general, C++ does not allow you to cast someclass<T> to someclass<U> as a template someclass might be specialized for U. It doesn't matter how T and U are related. This mean that the implementation and thus the object layout might be different.

Sadly, there is no way to tell the compiler in which cases the layout and/or behaviour didn't change and the cast should be accepted. I imagine it could be very useful for std::shared_ptr and other use-cases, but that is not going to happen anytime soon (AFAIK).

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
  • 1
    `std::shared_ptr` has [`std::X_pointer_cast`](http://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast). It's a non-member function and explicitly added functionality, though. – dyp Oct 01 '13 at 19:38
  • @DyP Yes, but it returns a new `shared_ptr`, it doesn't cast the existing instance by changing the type but pointing to the same memory location. – Daniel Frey Oct 01 '13 at 19:42
  • @DanielFrey What's the difference? `A a; (B)b;` does the same thing for some convertible types `A` and `B`. Only for fundamental types, pointers and references, the cast produces not an object (temporary) but just a value. – dyp Oct 01 '13 at 19:43
  • @DyP I have a hard time expressing it, I don't know why. But to me there are conversions (which create new objects) and casts, which are basically part of the type-system but don't really generate code. Hm, I need to work on that, but I'm quite tired right now :) – Daniel Frey Oct 01 '13 at 19:50
  • I think I see what you mean. – dyp Oct 01 '13 at 19:54
  • How is `there is not way to tell the compiler in which cases the layout and/or behaviour didn't change ` if this is all in declarations? – Hi-Angel Dec 21 '17 at 00:00
  • @Hi-Angel The behavior is *not* in the declarations, it might be in a different translation unit. – Daniel Frey Dec 21 '17 at 10:10
  • 1
    @Hi-Angel Also, probably even more relevant, the compiler *could maybe* figure out layout-compatibility, but it simply does not do that as the standard does not allow it. My sentence in the answer does not refer to the compiler, it refers to *you*! *You* have no way to tell the compiler that two template instantiations are compatible. – Daniel Frey Dec 21 '17 at 10:18
  • Thanks for clarification. Just want to address concern about a different translation unit — it is still described by declaration. Obviously though, if the translation unit modifies something that's declared as `const`, it's UB. And this isn't specific to templates — it's possible by simply casting off the qualifier in function accepting a `const`. As far as the compiler concerned — everything it needs is in `.h` files. – Hi-Angel Dec 21 '17 at 17:42
  • @Hi-Angel You don't know about the invariants that the `const T` implies. Even if the layout of the members is compatible, `someclass` might allow all values that T might have, while `someclass` might only accept a sub-set of those values. These things are allowed by the standard, whether they are likely or useful does not matter. – Daniel Frey Dec 21 '17 at 17:56
7
void fn(const vector<const char*>)

As the top-level const qualifier is dropped for the function type, this is (at the call site) equivalent to:

void fn(vector<const char*>)

Both of which request a copy of the passed vector, because Standard Library containers follow value semantics.

You can either:

  • call it via fn({vc.begin(), vc.end()}), requesting an explicit conversion
  • change the signature to, e.g. void fn(vector<const char*> const&), i.e. taking a reference

If you can modify the signature of fn, you can follow GManNickG's advice and use iterators / a range instead:

#include <iostream>
template<typename ConstRaIt>
void fn(ConstRaIt begin, ConstRaIt end)
{
    for(; begin != end; ++begin)
    {
        std::cout << *begin << std::endl;
    }
}

#include <vector>
int main()
{
    char arr[] = "hello world";
    std::vector<char *> vc;
    for(char& c : arr) vc.push_back(&c);

    fn(begin(vc), end(vc));
}

This gives the beautiful output

hello world
ello world
llo world
lo world
o world
 world
world
orld
rld
ld
d

The fundamental issue is to pass around Standard Library containers. If you only need constant access to the data, you don't need to know the actual container type and can use the template instead. This removes the coupling of fn to the type of container the caller uses.

As you have noticed, it's a bad idea to allow access of a std::vector<T*> through a std::vector<const T*>&. But if you don't need to modify the container, you can use a range instead.

If the function fn shall not or cannot be a template, you could still pass around ranges of const char* instead of vectors of const char. This will work with any container that guarantees contiguous storage, such as raw arrays, std::arrays, std::vectors and std::strings.

Community
  • 1
  • 1
dyp
  • 38,334
  • 13
  • 112
  • 177
2

To work around the issue, you could implement your own template wrapper:

template <typename T> class const_ptr_vector;

template <typename T> class const_ptr_vector<T *> {
    const std::vector<T *> &v_;
public:
    const_ptr_vector (const std::vector<T *> &v) : v_(v) {}
    typedef const T * value_type;
    //...
    value_type operator [] (int i) const { return v_[i]; }
    //...
    operator std::vector<const T *> () const {
        return std::vector<const T *>(v_.begin(), v_.end());
    }
    //...
};

The idea is that the wrapper provides an interface to the referenced vector, but the return values are always const T *. Since the referenced vector is const, all the interfaces provided by the wrapper should be const as well, as illustrated by the [] operator. This includes the iterators that would be provided by the wrapper. The wrapper iterators would just contain an iterator to the referenced vector, but all operations that yield the pointer value would be a const T *.

The goal of this workaround is not to provide something that can be passed to a function wanting a const std::vector<const T *> &, but to provide a different type to use for the function that provides the type safety of such a vector.

void fn(const_ptr_vector<char *> cvcc)
{
}

While you could not pass this to a function expecting a const std::vector<const T *> &, you could implement a conversion operator that would return a copy of a std::vector<const T *> initialized with the pointer values from the underlying vector. This has also been illustrated in the above example.

jxh
  • 69,070
  • 8
  • 110
  • 193
  • Nice, but you still cant pass this wrapper to a function expecting a vector. You'd need something like C#'s IEnumerable and use it everywhere. –  Oct 01 '13 at 19:14
  • @jdv-JandeVaan: I wasn't trying to solve the conversion problem, but providing a work around via a different type to use in its place. However, I have updated the answer with a conversion operator that creates a copy to `std::vector`. – jxh Oct 01 '13 at 20:11
-2

As the FQA suggests, this is a fundamental flaw in C++.

It appears that you can do what you want by some explicit casting:

vector<char*> vc = vector<char *>(); 
vector<const char*>* vcc = reinterpret_cast<vector<const char*>*>(&vc);
fn(*vcc);

This invokes Undefined Behavior and is not guaranteed to work; however, I am almost certain it will work in gcc with strict aliasing turned off (-fno-strict-aliasing). In any case, this can only work as a temporary hack; you should just copy the vector to do what you want in a guaranteed manner.

std::copy(vc.begin(), vc.end(), std::back_inserter(vcc));

This is OK also from the performance perspective, because fn copies its parameter when it's called.

anatolyg
  • 26,506
  • 9
  • 60
  • 134
  • 4
    It's not a fundamental flaw, it's a fundamental characteristic. – bstamour Oct 01 '13 at 18:28
  • 2
    That's what my mother always told me anyway. – VoronoiPotato Oct 01 '13 at 18:40
  • 3
    I consider reinterpret_cast on containers non-idiomatic C++. And that's putting it mildly. –  Oct 01 '13 at 19:10
  • 1
    It's a fundamental compromise to avoid making the type system even more complex. After all, this is in the FQA section claiming the type system is too complex. The compromise makes sense, the FQA does not. – Ben Voigt Oct 01 '13 at 20:50
  • 1
    All pointer types are basically identical as far as storage, so I don't find this that different from a C-style cast. It's potentially unsafe but I bet it works 99% of the time. If C++ had list comprehensions the `reinterpret_cast` option wouldn't be so appealing, but it takes quite a bit of code to constify a container. – Walter Nissen May 17 '19 at 16:07