Search code examples
c++c++14visitor-patternperfect-forwarding

Using std::forward on casted arguments


This is a similar question to Using std::forward on sub fields, but the answer there doesn't seem to apply in my case.

Consider this code:

template<class Base, class F>
void visit(Base&&, const F&) {
    throw std::bad_cast();
}

template<class Derived, class... Rest, class Base, class F>
void visit(Base&& base, const F& f) {
    if (auto *as_derived = dynamic_cast<Derived *>(&base)) {
        return f(std::forward<Base>(*as_derived));
    } else {
        return visit<Rest...>(std::forward<Base>(base), f);
    }
}

My goal is that the following test case work:

struct Animal {
    virtual ~Animal() {}
};
struct Cat : Animal {
    void speak() & { puts("meow"); }
    void yowl() && { puts("MEOW!"); }
};
struct Dog : Animal {
    void speak() & { puts("woof"); }
    void yowl() && { puts("WOOF!"); }
};

int main() {
    Animal *a = new Cat();
    Animal *b = new Dog();
    visit<Cat, Dog>(*a, [](auto&& a){ std::forward<decltype(a)>(a).speak(); });
    visit<Cat, Dog>(*b, [](auto&& a){ std::forward<decltype(a)>(a).speak(); });
    visit<Cat, Dog>(std::move(*a), [](auto&& a){ std::forward<decltype(a)>(a).yowl(); });
    visit<Cat, Dog>(std::move(*b), [](auto&& a){ std::forward<decltype(a)>(a).yowl(); });
}

Desired output: "meow" "woof" "MEOW!" "WOOF!". Notice that the functions speak and yowl are non-virtual; this is required in my original code because they are actually templates, and templates can't be virtual.

The problem with this code as written here is that std::forward<Base>(*as_derived) doesn't merely change the ref-qualifiers and const-qualifiers on *as_derived to enable perfect forwarding; it actually casts the type back up to Base&, nerfing the whole point of visit!

Is there a standard library function that does what I want std::forward to do — namely, change the ref-qualifiers and const-qualifiers on *as_derived to match the ones that would be perfect-forwardly deduced from std::forward<Base>?

If there's no standard library function, how could I write a "perfect forward a child type" function for my own use?

The Wandbox link above contains something that "works" for this test case, but it doesn't preserve constness and it doesn't look elegant at all.


Solution

  • There isn't anything in the standard for this. But it isn't hard to write. Just annoying. What you need to do is write a trait that gives you the type to pass into forward - basically you want to match the cv-qualifications and reference-ness of Derived to what Base is, and then pass that type into forward:

    return f(std::forward<match_ref_t<Base, Derived>>(*as_derived));
    

    A simple implementation, that can almost certainly be made more concise, is just:

    template <class From, class To>
    struct match_ref {
        using type = To;
    };
    
    template <class From, class To>
    using match_ref_t = typename match_ref<From, To>::type;
    
    template <class From, class To>
    struct match_ref<From&, To> {
        using type = match_ref_t<From, To>&;
    };
    
    template <class From, class To>
    struct match_ref<From&&, To> {
        using type = match_ref_t<From, To>&&;
    };
    
    template <class From, class To>
    struct match_ref<From const, To> {
        using type = match_ref_t<From, To> const;
    };
    
    template <class From, class To>
    struct match_ref<From volatile, To> {
        using type = match_ref_t<From, To> volatile;
    };
    
    template <class From, class To>
    struct match_ref<From const volatile, To> {
        using type = match_ref_t<From, To> const volatile;
    };
    

    Or, I guess:

    template <class Check, template <class> class F, class T>
    using maybe_apply = std::conditional_t<Check::value, F<T>, T>; 
    
    template <class From, class To> 
    struct match_ref {
        using non_ref = std::remove_reference_t<From>;
        using to_cv = maybe_apply<std::is_const<non_ref>, std::add_const_t,
              maybe_apply<std::is_volatile<non_ref>, std::add_volatile_t,
              To>>;
    
        using type = std::conditional_t<
            std::is_lvalue_reference<From>::value,
            to_cv&,
            std::conditional_t<
                std::is_rvalue_reference<From>::value,
                to_cv&&,
                to_cv>
            >;
    };
    
    template <class From, class To> 
    using match_ref_t = typename match_ref<From, To>::type;