ARTICLE AD BOX
I'm not sure if I understand you right, but I will propose this:
#include <string> #include <variant> struct A { virtual ~A() = default; std::string str{"A"}; }; struct B { virtual ~B() = default; std::string str{"B"}; }; // const-correct string, adjusting return type to constness template <typename T> using cc_string = std::conditional_t<std::is_const_v<std::remove_reference_t<T>>, std::string const, std::string>; struct V : public std::variant<A, B> { // returning a const-correct reference to the common type template <typename Self> auto&& get_string(this Self&& self) { return std::visit([](auto&& s) -> cc_string<Self>& { return s.str; }, std::forward<Self>(self)); } };First, note that I remove the std::string inheritance, as it is not designed to be inherited.
Then, the return type of the lambda must be the same for all possibilities of the variant, in this case it's related to std::string.
My guess, then, is that you want to get a writable reference to the input object string if it's not const, and a read-only one otherwise.
Instead of letting the lambda deduce it, I propose to tell it explicitly, using a trailing return type and a helper template type cc_string that will get the correct constness from the explicit object parameter type (here Self).
This gives:
int main() { A a; B b; V v{a}; std::puts(v.get_string().c_str()); v.get_string() = "changed"; std::puts(v.get_string().c_str()); V const w(b); // w.get_string() = "changed"; std::puts(w.get_string().c_str()); std::puts(V{b}.get_string().c_str()); }As you see, the const object string is read-only.
Unfortunately, it's not as readable as you might have expected...
If you absolutely want to inherit from std::string, and thus need to upcast then, just upcast using my helper type:
struct V : public std::variant<A, B> { // returning a const-correct reference to the common type template <typename Self> auto&& get_string(this Self&& self) { return std::visit( [](auto&& s) -> cc_string<Self>& { return static_cast<cc_string<Self>&>(s); }, std::forward<Self>(self)); } };In answer to last OP comment, we can go further:
#include <concepts> template <typename Like, typename Base, typename Derived> auto&& forward_upward_like(Derived&& x) requires std::derived_from<std::remove_reference_t<Derived>, Base> { return static_cast<decltype(std::forward_like<Like>(std::declval<Base>()))>( x); } struct V : public std::variant<A, B> { // returning a const-correct reference to the common type template <typename Self> auto&& get_base(this Self&& self) { return std::visit( [](auto&& s) -> decltype(auto) { return forward_upward_like<Self, Base>(s); }, std::forward<Self>(self)); } };I'm casting s to its base subobject getting constness and value category from Self with std::forward_like. But it requires an object of the appropriate type as argument, so I create it with std::declval<Base>().
Then I give the type of the std::forward_like as argument to the static_cast.
Actually, the purpose of decltype(std::forward_like<Like>(std::declval<Base>())) is to take the type of Base and give him the constness and "referenceness" of the referenced type of Like.
Finally, I'm using decltype(auto) as lambda trailing return type in order to get the expected type on output and we're done.
The running example is THERE
NB By looking at the proposed implementation of std::forward_like in cppreference, it's quite straightforward to get ride of std::declval and directly manipulate types.
template <typename Like, typename T> struct same_cref_as { private: using referenced_like_t = std::remove_reference_t<Like>; using same_const_t = std::conditional_t<std::is_const_v<referenced_like_t>, std::add_const_t<T>, T>; public: using type = std::conditional_t<std::is_lvalue_reference_v<Like>, same_const_t&, same_const_t&&>; }; template <typename Like, typename T> using same_cref_as_t = same_cref_as<Like, T>::type; template <typename Like, typename Base, typename Derived> auto&& forward_upward_like(Derived&& x) requires std::derived_from<std::remove_reference_t<Derived>, Base> { return static_cast<same_cref_as_t<Like, Base>>( x); }One could argue that the intent is clearer than using same_cref_as_t = decltype(std::forward_like<Like>(std::declval<Base>()));, but it's also more code (which may not matter inside a library in fact).
