Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> you still can't have a non-template function that works on a const std::shared_ptr<const *base_type> or any derived type

I guess you meant, you can't have a function that accepts a shared_ptr to the base type without indirectly using template machinery?

Because you can most definitely have a function that does this that is not a function template.

    #include <memory>
    #include <iostream>
    
    
    struct foo {
     virtual ~foo() = default;
     virtual void bark() const { std::cout << "fork\n"; }
    };
    
    
    struct bar : foo {
     void bark() const override { std::cout << "bark\n"; }
    };
    
    
    void poke_so_it_barks(std::shared_ptr<foo const> const thing) {
     thing->bark();
    }
    
    
    int main() {
     poke_so_it_barks(std::make_shared<foo>());
     poke_so_it_barks(std::make_shared<bar>());
    }


Oops, I picked a bad example it seems... Apparently `shared_ptr` does some magic to simulate covariance through type conversion. It still breaks in other cases (a function which returns a `shared_ptr<derived>` can't override a function which returns a `shared_ptr<base>`, unlike with derived* and base* ), but it still probably covers most real-world cases.

If I had stuck with std::vector my point would have been better. Even though theoretically a `const std::vector<derived* >` IS-A `const std::vector<base* >`, that is, a read-only view of a collection of derived elements behaves the same as a read-only view of a collection of base elements, you can't pass the first to a function expecting the second in C++, unless you make your own function a template.


In that case, you have a point. The closest you can get to covariance and contravariance is simulating it with implicit conversion operators/constructors (which then often need to be templates).

But I don't see how this could be implemented without completely changing large parts of the language.

In the one case of covariance that actually exists, overriding `virtual base* factory()` with `derived* factory() override`, the overridden function knows that the function it overrides returns a `base* `, so it can just return a `base* `, too, and callers through the subclass pointer know that they can adjust the return value back to a `derived* `.

But even the closest cousin, argument contravariance (i.e. overriding `virtual foo(derived* )` with `foo(base* ) override`) doesn't seem feasible. Callers through the base pointer will pass a `derived* `, so the overridden function has to expect to be passed a `derived* `, and there is no adjustment that can be made to a `base* ` to turn it into a valid `derived* ` statically.

The vector example is even more impossible. Since the function expects all members of the `std::vector<base* > const` argument to be `base* `, a caller with a `std::vector<derived* >` would have to allocate a new vector and adjust each individual pointer, which would be a terrible hidden runtime cost. (If they had added a constructor to accept a range to complement the one that takes an iterator pair, that's exactly what would have happened, though.)


> Since the function expects all members of the `std::vector<base* > const` argument to be `base* `, a caller with a `std::vector<derived* >` would have to allocate a new vector and adjust each individual pointer, which would be a terrible hidden runtime cost.

I don't think I understand your point. A function which expects a `base* ` can already be passed a `derived* ` without any problems. Doing the same for a collection of `base* ` to a collection of `derived* ` is not fundamentally different, but it does come with significant complications. For one, the language would have to be sure that the collection is read-only, since a function which wants to add something to the collection can't be called with a collection of a different type.

More significantly, the language itself has no idea that `std::vector<T>` is a collection of Ts, while `std::function<T>` is not a collection. In principle, it may be possible that it could look at all of the members and decide each template argument's variance. Even if that is theoretically possible (I'm not sure it is), it is certainly far too complicated to be a useful feature.

That leaves us with the option of some kind of annotation for variance in template parameters (or at use-time, as in Java).


> A function which expects a `base* ` can already be passed a `derived* ` without any problems. Doing the same for a collection of `base* ` to a collection of `derived* ` is not fundamentally different

Since C++ has multiple inheritance, derived* differs from base* not just by reinterpretation, the pointer value could be different.

A single pointer can trivially be adjusted, but a heap-allocated, potentially huge array of pointers would require an equally huge heap allocation to store the adjusted pointers.


Got it, thanks for bearing with me! I wasn't aware of this complexity of multiple inheritance (I guess this is another example of how orthogonal the language is :) ).


How could the type system reason about when covariance is valid?


It could potentially use const-ness for these use cases, though I'm not sure that would be tractable.

More realistically, it could allow the programmer to express it explicitly, as in C# (C<in T, out V> is covariant in T and contravariant in V) or Java (C<? extends T,? super V> is covariant in the first parameter, contravariant in the second). This would not solve the problem that the co-/contra-variance of some types may depend on their const-ness, but it would allow at least a covariant, always read-only, std::vector_view<T> (similar to IEnumerable<in T> in C#, which can also be implemented by the invariant List<T>/std::vector<T>).




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: