user1709708
user1709708

Reputation: 1577

How to const_cast a vector of const pointers to a vector of non-const pointers?

I have a function that's looking for an element inside a custom list implementation. To make it const-correct, I can do this neat overload and even use a one-liner to reuse the very same implementation, thus not having to duplicate logic.

const MyObject* findObject(const MyList& list)
{
  //...
}

MyObject* findObject(MyList& list)
{
  return const_cast<MyObject*>(findObject(const_cast<const MyList&>(list)));
}

The problem is, what do I do once I need to return multiple element pointers inside a vector barring unsave/non-portable hacks like reinterpret_cast?

std::vector<const MyObject*> findObject(const MyList& list)
{
  //...
}

std::vector<MyObject*> findObject(MyList& list)
{
  // this is sth I'm looking for:
  const_element_cast<std::vector<MyObject*>>( findObject( const_cast<const MyList&>(list)) );
}

Upvotes: 1

Views: 1323

Answers (2)

scx
scx

Reputation: 3937

Option 1

If you are willing to pay the loop + copy price (vs. copy-pasting code) :

std::vector<const thing*> c_vec = static_cast<const me*>(this)->my_const_func();
std::vector<thing*> ret;

for (const thing* t : c_vec) {
    // Yes you can do this, since this func isn't const
    // so you are guaranteed to be inside non-const context.
    // See Scott Meyers.

    ret.push_back(const_cast<thing*>(t));
}
return ret;

Option 2

[edit] Here is another alternative if you do not want to pay the loop + copy price.

Assuming you are working with a cpp file (not header-only). Inside your compile unit (.cpp), add a templated getter.

template <class T, class MyObj>
std::vector<T*> getter_imp(MyObj& obj, int someval) {
    assert(someval <= 42);

    std::vector<T*> ret;
    for (auto& i : obj._my_vec) {
        if (i < someval) {
            ret.push_back(&i);
        }
    }
    return ret;
}

To keep things clean and "hidden", you can friend that function in your class.

template <class T, class MyObj>
friend std::vector<T*> getter_imp(MyObj&, int);

Now, you can call the getter from both const and non-const member functions. Without copy-paste (aka the bane of our lives).

std::vector<const int*> get(int v) const {
    return getter_imp<const int>(*this, v);
}
std::vector<int*> get(int v) {
    return getter_imp<int>(*this, v);
}

Whats happening here?

You are encoding const inside templated arguments. Lets say you call from the const function, T = const int and MyObj = const your_class. Inside the function, const gets deduced into auto& as well.

When calling from a non-const context, the template arguments and auto& aren't const. So it works.

Upvotes: 0

Christian Hackl
Christian Hackl

Reputation: 27528

The easiest solution is to forget about const_cast and just implement both overloads explicitly. This is what standard-library implementations do.

For example, std::vector has a const and non-const version of operator[]. Both VC++ and GCC have duplicate implementations for that operator (see include/vector or stl_vector.h files, respectively); neither resorts to any const_cast tricks to duplicate the behaviour.

Now, if your findObject implementation is very big and complicated, then the first choice should be to make it simpler. As a temporary workaround, you may consider implementing both overloads in terms of an internal (private or otherwise inaccessible) template function, using a decltype trailing return type to get the correct const or non-const return type via the argument. Here is a simple example:

#include <iostream>
#include <vector>
#include <typeinfo> // just to demonstrate what's going on

// simple example data structure:
struct MyObject {};
struct MyList
{
    MyObject elements[3] = { MyObject {}, MyObject {}, MyObject {} };
};

namespace internal
{
    // &list.elements[0] will be either MyObject const* or MyObject*
    template <class ConstOrNonConstList>
    auto doFindObject(ConstOrNonConstList& list) -> std::vector<decltype(&list.elements[0])>
    {
        // let's say there was an immensely complicated function here,
        // with a lot of error potential and maintenance nightmares
        // lurking behind a simple copy & paste solution...

        // just to demonstrate what's going:
        std::cout << typeid(decltype(&list.elements[0])).name() << "\n";

        std::vector<decltype(&list.elements[0])> result;
        for (auto&& element : list.elements)
        {
            result.push_back(&element);
        }
        return result;
    }
}

std::vector<const MyObject*> findObject(const MyList& list)
{
    std::cout << "const version\n";
    return internal::doFindObject(list);
}

std::vector<MyObject*> findObject(MyList& list)
{
    std::cout << "non-const version\n";
    return internal::doFindObject(list);
}

int main()
{
    MyList list1;
    MyList const list2;
    auto obj1 = findObject(list1);
    auto obj2 = findObject(list2);
}

Example output (depending on what kind of names typeid produces on your implementation):

non-const version
struct MyObject *
const version
struct MyObject const *

But frankly, I would not do this. It seems over-engineered, and it's perhaps a bit too clever. Overly clever code is rarely a good idea, because it confuses people.

Upvotes: 2

Related Questions