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