c++pointersvectorconst-cast

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)) );
}

Solution

  • 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.