1

Situation

I have some existing functions which expect containers which can be iterated and which have certain value_type. The value_type can be a value or some (smart) pointer.

All the value_type have traits implementations for their base types, f.e.

template <>
struct Trait<A> {
    static void test(A const& v) {
        std::cout << "A " << v.i << std::endl;
    }
};

The existing functions look like this:

template <typename T>
void foreach(T&& c) {
    for (auto it = c.begin(); it != c.end(); ++it)
    {
        auto&& v = *it;
        Trait<...>::test(it); // optional
        Trait<...>::test(v);
    }
}

At the moment there's a copy/paste of each trait per type and for all possible pointer and non pointer types.

Since there are many possible pointer and non pointer types i am looking for a way to get rid of the duplicates.

Possible Solutions

I came up with two approaches to instantiate the traits and dereference the value_type automatically.

The first uses a wrapper trait which dereferences on demand and forwards to the actual implementation. The second adds a template function to each function in the trait where the dereferencing is done. I haven't thought of a third approach yet.

Live demo of approach 1.

Live demo of approach 2.

Thoughts

I like approach 1 more because it seems easier for users and doesn't contaminate the actual traits with the dereferencing wrappers from approach 2 but i'm wondering if it's really worth the effort or if i should just copy/paste each trait for all possible pointer and non pointer types, e.g.

template <>
struct Trait<A> {
    static void test(A const& v) {
        std::cout << "A " << v.i << std::endl;
    }
};
template <>
struct Trait<A*> {
    static void test(A const* v) {
        std::cout << "A " << v->i << std::endl;
    }
};
template <>
struct Trait<std::unique_ptr<A>> {
    static void test(std::unique_ptr<A> const& v) {
        std::cout << "A " << v->i << std::endl;
    }
};
template <>
struct Trait<std::shared_ptr<A>> {
    static void test(std::shared_ptr<A> const& v) {
        std::cout << "A " << v->i << std::endl;
    }
};
// ...

Besides the additional template wrapper function in the traits i also don't like approach 2 because instantiating the trait either needs a manual decltype invocation or the workaround with a value instance of the trait type:

GetTrait<decltype(v)>::test(v); // decltype is a bit ugly
getTrait(v).test(v); // the instance seems a bit ugly

I'm hoping for a better and easier approach than this two but couldn't think of one yet.

Details and Comparisons

The structure and helpers for both approaches are mostly identical:

Concepts and methods to determine if dereferencing is needed and to actually do it:

template<typename T>
concept Dereferenceable = requires (T x) { *x; };
template<typename T>
concept NonDereferenceable = !Dereferenceable<T>;

template <typename T>
    requires Dereferenceable<T> || NonDereferenceable<T>
struct Dereferenced
{};
template <Dereferenceable T>
struct Dereferenced<T>
{
    using type = typename Dereferenced < std::remove_reference_t<decltype(*std::remove_reference_t<T>{}) >> ::type;
};
template <NonDereferenceable T>
struct Dereferenced<T>
{
    using type = std::remove_reference_t<T>;
};

template <NonDereferenceable T>
auto&& dereferenceIfNeeded(T&& v) {
    return v;
}
template <Dereferenceable T>
auto&& dereferenceIfNeeded(T&& v) {
    return dereferenceIfNeeded(*v);
}

Test types:

struct A { int i; };
struct B { int j; };

Traits for approach 1:

namespace impl
{
    template <typename T>
    struct Trait {};

    template <>
    struct Trait<A> {
        static void test(A const& v) {
            std::cout << "A " << v.i << std::endl;
        }
    };
    template <>
    struct Trait<B> {
        static void test(B const& v) {
            std::cout << "B " << v.j << std::endl;
        }
    };
}

template <typename T>
using GetTrait = impl::Trait<std::remove_cvref_t<typename Dereferenced<T>::type>>;

template <typename T>
struct Trait {
    static void test(T const& v) {
        GetTrait<T>::test(dereferenceIfNeeded(v));
    }
};

Traits for approach 2:

template <>
struct Trait<A> {
    static void test(A const& v) {
        std::cout << "A " << v.i << std::endl;
    }
    template <typename T>
    static void test(T const& v) {
        test(dereferenceIfNeeded(v));
    }
};
template <>
struct Trait<B> {
    static void test(B const& v) {
        std::cout << "B " << v.j << std::endl;
    }
    template <typename T>
    static void test(T const& v) {
        test(dereferenceIfNeeded(v));
    }
};

template <typename T>
using GetTrait = Trait<std::remove_cvref_t<typename Dereferenced<T>::type>>;

template <typename T>
auto getTrait(T&& t) {
    return Trait<std::remove_cvref_t<typename Dereferenced<T>::type>>();
}

Testloop for approach 1:

template <typename T>
void foreach(T&& c) {
    for (auto it = c.begin(); it != c.end(); ++it)
    {
        auto&& v = *it;
        Trait<decltype(it)>::test(it);
        Trait<decltype(v)>::test(v);
    }
}

Testloop for approach 2:

template <typename T>
void foreach(T&& c) {
    for (auto it = c.begin(); it != c.end(); ++it)
    {
        auto&& v = *it;
        GetTrait<decltype(v)>::test(v); // decltype is a bit ugly
        getTrait(v).test(v); // the instance seems a bit ugly
    }
}

The testcases:

void test()
{
    std::vector<A*> vrpa = { new A{2} };
    std::vector<std::unique_ptr<A>> vupa; vupa.push_back(std::unique_ptr<A>{new A{ 3 }});
    std::vector<std::shared_ptr<A>> vspa = { std::shared_ptr<A>{new A{4}} };
    std::vector<A> va = { A{5} };

    std::list<A*> lrpa = { new A{12} };
    std::list<std::unique_ptr<A>> lupa; lupa.push_back(std::unique_ptr<A>{new A{ 13 }});
    std::list<std::shared_ptr<A>> lspa = { std::shared_ptr<A>{new A{14}} };
    std::list<A> la = { A{15} };

    std::vector<B*> vrpb = { new B{112} };
    std::vector<std::unique_ptr<B>> vupb; vupb.push_back(std::unique_ptr<B>{new B{ 113 }});
    std::vector<std::shared_ptr<B>> vspb = { std::shared_ptr<B>{new B{114}} };
    std::vector<B> vb = { B{115} };

    std::list<B*> lrpb = { new B{1112} };
    std::list<std::unique_ptr<B>> lupb; lupb.push_back(std::unique_ptr<B>{new B{ 1113 }});
    std::list<std::shared_ptr<B>> lspb = { std::shared_ptr<B>{new B{1114}} };
    std::list<B> lb = { B{1115} };

    foreach(vrpa);
    foreach(vupa);
    foreach(vspa);
    foreach(va);

    foreach(lrpa);
    foreach(lupa);
    foreach(lspa);
    foreach(la);

    foreach(vrpb);
    foreach(vupb);
    foreach(vspb);
    foreach(vb);

    foreach(lrpb);
    foreach(lupb);
    foreach(lspb);
    foreach(lb);
}

Solution

Live demo of the solution from Deduplicator.

1 Answer 1

2

Yes, you really should factor out adapting the argument, but dereferenceIfNeeded() is a mouthful. Why not tryDeref()?
As you want to tackle all (smart)-pointer, and references, what about std::reference_wrapper?
What if you have multiple levels? And what if a pointer is null? Ok, just assuming it isn't can be valid.
Finally, simplify:

constexpr decltype(auto) tryDeref(auto&& v) {
    if constexpr (std::is_same_v<std::decay_t<decltype(v)>, decltype(std::ref(v))>)
        return tryDeref(v.get());
    else if constexpr (requires { *v })
        return tryDeref(*v);
    else
        return v;
}

If there are few call-sites, or adapting the argument like that is rarely wanted, let the caller do it.
Otherwise, the callee should.

Still, don't force the caller to figure out the final adapted type, and pass it along. Do it in the callee, which also allows you to consolidate adapting the arguments:

template <class = void>
struct Trait {};
template <>
struct Trait<> {
    static void test(auto&& v) {
        using T = std::decay_t<decltype(tryDeref(v))>;
        Trait<T>::test(tryDeref(v));
    }
};

template <>
struct Trait<A> {
    static void test(A const& v) {
        std::cout << "A " << v.i << "\n";
    }
};
template <>
struct Trait<B> {
    static void test(B const& v) {
        std::cout << "B " << v.j << "\n";
    }
};

As an aside, don't flush if you don't need to. It's a waste.

1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.