The problem with that is simple:
What is the common interface?
If they have a common type, like one is descended from the other, views::concat() is intelligent enough to go there, and you still have dynamic dispatch for virtual member functions.
If they are both descended from the same base (but neither is the base), you would be able to get the above by using views::transform().
Otherwise, you would need to views::transform() them into a proxy-object which can reference either, and dispatches the members you care about appropriately.
While std::variant cannot be it, unless you are fine with a copy, std::variant storing std::reference_wrappers may work.
auto map = ranges::views::transform([](auto& x) {
return std::variant<std::reference_wrapper<A>, std::reference_wrapper<B>>(std::ref(x)); });
for (const auto& v : ranges::views::concat(as | map, bs | map))
std::visit([](auto x) {
auto& v = x.get();
foo(v);
}, v);
Alternative
Use static_for_each:
template <class F, class T>
constexpr void static_for_each(F&& f, T&& t) {
std::apply([&](auto&&... x){
((f(std::forward<decltype(x)>(x)), void()), ...);
}, t);
}
static_for_each([&](auto&& c){
for (auto&& v : c)
foo(v);
}, std::forward_as_tuple(as, bs));