Historically in C++ it was considered to be more performant to dispatch function calls on the base type of its arguments, not the concrete types.
Consider for simplicity member functions A::f(x...) in C++ as free functions where the first argument is fixed to be of type A, as in f(A, x...). If you want to emulate multiple dispatch for just 2 arguments f(A, B), you already find yourself in visitor pattern-land.
And the other issue with C++ is that it does not allow open functions. In the visitor pattern you necessarily have to implement f(A, B) for _all_ subtypes of A and B.
In Julia open functions + multiple dispatch make life quite a bit easier than in C++:
julia> abstract type Animal end
julia> abstract type Color end
julia> struct Dog <: Animal end
julia> struct Cat <: Animal end
julia> struct Brown <: Color end
julia> struct White <: Color end
julia> f(::White, ::Cat) = println("Hello white cat")
julia> f(::Brown, ::Dog) = println("Hello brown dog")
julia> for (color, animal) in [(White(), Cat()), (Brown(), Dog())]
f(color, animal)
end
Hello white cat
Hello brown dog
Note that array type is here is Vector{Tuple{Color,Animal}}, which would correspond to type erasure in C++.Lastly, the modern C++ version with std::variant + std::visitor is probably not really multiple dispatch, since it has a union storage type under the hood.
It adds cognitive load when done badly, but reduces it when done well and consistently. The knee jerk dismissal of different approaches is probably the main reason why programming as an art has been by and large stagnant for decades.