Overloading method to accept const or non-const parameter without duplicating code
Asked Answered
B

1

5

I have a project where I'm putting objects in an unordered_map that can logically chain together with other objects in the map. To represent this chain, I store a reference to the next object in the chain's map key, so I end up with something resembling a linked list within the map:

class Foo
{
    int *next;
    // other fields, methods, etc.
};
std::unordered_map<int, Foo> foos;

I've added a helper method that follows the linked list for an object and returns pointers to the following objects in a vector:

class Foo
{
    int *next;

    std::vector<Foo *> following(std::unordered_map<int, Foo> &foos) const
    {
        std::vector<Foo *> result;
        const Foo *current = this;
        while (current->next)
        {
            auto nextIt = foos.find(*current->next);
            if (nextIt == foos.end()) break;

            result.push_back(&nextIt->second);
            current = &nextIt->second;
        }
        return result;
    }

I also want to have a version of this helper that works on a const reference to the map and returns const pointers, i.e. std::vector<const Foo *> following(const std::unordered_map<int, Foo> &foos) const. I can of course create this just by duplicating the code, but is there a way to achieve this without duplication through templating or something similar?

Beera answered 31/10 at 12:1 Comment(4)
The standard way to do it is to write it for const reference and than the non-const overload casts non-const reference to const reference and directs the call to the const overload. In C++23, you also have deducing this, which can allow you to template your function on constness. You can also emulate deducing this with an explicit template parameter.Jacklighter
@Jacklighter Aside from this approach being prone to making mistakes resulting in undefined behavior (because it requires a const_cast), it won't work here because OP wants the inner type of the return value to be conditionally-const. A const_cast can't do that and a C-style cast or reinterpret_cast will result in undefined behavior.Pantomimist
@Jacklighter Also, it is not this that is conditionally-const here.Pantomimist
@Pantomimist const_cast is perfectly safe in this context. It is used in countless codebases in this exact way. It is safe because it is restricted to this single overload and it casts non-const into const, not the other way around. Of course, in the code as OP has written it, this method and deducing this are not an option but he might adjust his code if his application allows it. I have listed him all the possible options. "emulating deducing this" is basically what you have done in your answer and that is exactly what I had in mind.Jacklighter
P
7

Yes you can do this with a little template hackery.

template <typename K>
auto following(K &foos) const
{
    using elem = std::remove_reference_t<decltype((foos.begin()->second))>;
    std::vector<elem *> result;

    const Foo *current = this;
    while (current->next)
    {
        auto nextIt = foos.find(*current->next);
        if (nextIt == foos.end()) break;
        result.push_back(&nextIt->second);
        current = &nextIt->second;
    }

    return result;
}

If you use C++20 or newer, you can restrict the template parameter to get better error messages:

template <typename K>
requires std::is_same_v<const K, const std::unordered_map<int, Foo>>
Palaeozoic answered 31/10 at 12:28 Comment(1)
Alternatively to a requires clause in C++20, a static_assert or SFINAE will also do in earlier versions.Pantomimist

© 2022 - 2024 — McMap. All rights reserved.