Covariance and typing

C++ supports covariant return types -- that is, class A can have function foo that returns a BaseClass pointer, and B can derive from A and override foo to have a return type of DerivedClass pointer.

Why couldn't this be extended to covariant members?

class Base {
};

class Derived : public Base {
};

class A {
protected:
    Base x;
};

class B : public A {
protected:
    Derived x;
};

With covariant members, this would make it so in instances of A, x is a Base, but in instances of B, x is a Derived. In vanilla C++, B's x would simply hide A's. All of A's code that B inherits using x should still work because Derived inherits from Base. Currently to do this you need boilerplate to take advantage of covariant returns:

class A {
protected:
    virtual Base& GetX() { return x; }
    Base x;
};

class B {
protected:
    virtual Derived& GetX() { return myX; }
    Derived myX;
};

Further boilerplate has to be written if you don't want x and myX to both be allocated in B, and you'll need to call GetX() everytime you want that member in your code rather than accessing it directly.

Covariant parameters would follow the same pattern although I'm not sure of a good use case for them other than for getting around C++ not having virtual operators (you may want to define an Assign member function to use in place of the assignment operator because it could be virtual and have it take a covariant parameter for what to assign to).

I'm curious if there are any OOP languages out there have tried to more strongly support the covariance relationship between classes. I've seen some variation in how classes inherit (whether for interface or code reuse) between languages but no difference as regards this behavior.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Covariant arguments aren't type safe

They need to be contravariant, or you wind up accepting things you shouldn't.

Sorta

They're not typesafe without a runtime check. You can dynamic_cast the parameter down to what you want and check if it worked.

Mutability precludes variance

Variance is incompatible with mutability. Consider:

class Base {};
class Derived : public Base {};
class Derived2 : public Base {};

class A {
public:
    Base x;
};

class B : public A {
public:
    Derived x;
};

void f(B *b, Derived2 d2) {
    A *a = b;
    a->x = d2;     // ouch!!
}

That is, covariance could only be allowed safely for const members.

On the other hand, your example seems to demonstrate that the subtyping rules for references and pointers are already broken beyond repair in C++. Consider the following extension to your code (where I assume that you actually meant B to subclass A):

class C : public A {
  void f(Derived2 d2) {
    Base &base = GetX();
    base = d2;     // ouch!!
  }
};

GCC accepts this without warning.

void f(B *b, Derived2 d2)


void f(B *b, Derived2 d2) {
    A *a = b;
    a->x = d2;     // ouch!!
}

I think that's workaroundable. Mainly my intent with the idea was that A could say by default create a NullFont object that didn't actually draw any text, but then B might make a TimesFont object that does, and the drawing code in A would be the same. So you'd use it within the class and access it directly.

C++'s mess of pointers obfuscates the issue, but in this specific instance an easy fix would be to disallow pointers to covariant members. That gets rid of the problem but still makes the use case I had in mind easy.

class C : public A {
  void f(Derived2 d2) {
    Base &base = GetX();
    base = d2;     // ouch!!
  }
};

That goes through gcc without warning because it's valid code. Base's assignment operator will run, which will be the default compiler generated one given your example, and assign base's members to d2's Base members (basically, the assignment occurs for the parts they have in common). This is because operators aren't virtual.

This is an often desireable behavior I use in my own code. Say you have three classes, Graph (an abstract base class), Bar Graph, and Pie Graph. Now I want my Bar Graph instance to show the same data as my Pie Graph instance. The Graph class we'll say carries a pointer to the data to be graphed, which Pie/Bar both inherit.

PieGraph* pPieGraph;
BarGraph* pBarGraph;

// ..Create a pie graph and a bar graph and store them in the pointers..

*pBarGraph = *pPieGraph; // Error! Compiler can't generate an assignment operator for this

Graph* graphpBarGraph;
*graphpBarGraph = *pPieGraph; // Success!

The compiler generated BarGraph assignment operator will expect another BarGraph. The compiler generated Graph assignment operator will expect another Graph, which PieGraph will satisfy.

Type safety and object layout

but in this specific instance an easy fix would be to disallow pointers to covariant members

So what would be a non-covariant member with your proposal?

That goes through gcc without warning because it's valid code.

Yes, and that's the problem. Because it breaks type safety and encapsulation - silently. Obviously, with covariant members it would become even worse.

But if that does not convince you, here is another problem with variant members: object layout. The whole C++ object model relies on the fact that the memory taken by a superclass is always a prefix of the memory of any subclass (except for virtual base classes). You cannot subclass slots in this model.

Object layout

To pick a nit: it's always a substring, but maybe not a prefix; with multiple (non-virtual) inheritance, only one of the superclasses can live at offset 0 within the subclass.

It would be possible to extend a const Base* slot into a const Derived*, but that would be much easier in the absence of multiple/virtual inheritance (as, otherwise, the compiler would have to insert casts wherever the slot is used in the subclass).

Yes, and that's the


Yes, and that's the problem. Because it breaks type safety and encapsulation - silently.

How? It's still type safe; the compiler will prevent you from assigning the Graph pointer in the example to some random object. I'm not really sure how it violates encapsulation either -- it's pragmatically the same as any language that lets you call a parent class version of a method on an object.

So what would be a non-covariant member with your proposal?

A member that you can't change the type of (polymorphically) in a subclass. Just like functions can be declared virtual, you would declare members as such.


The whole C++ object model relies on the fact that the memory taken by a superclass is always a prefix of the memory of any subclass (except for virtual base classes). You cannot subclass slots in this model.

One of C++'s main tenets is that you don't pay for features you don't use. I wouldn't be proposing an idea that could require an extra pointer on every object or an extra level of indirection if I seriously meant it as a suggestion for C++ itself, I just was using it as an example of a language that could perhaps be helped by the idea. It's pretty generalizable to every OOP language (except perhaps Eiffel as someone posted below). I acknowlege there maybe other nontopical reasons why the standards committee would reject it ;)

Isn't this called (object) slicing?

Isn't this called (object) slicing in the literature? Off the top of my head this is a problem in all subtyped languages, with assignment or not, because it can occur in copy construction/initialisation. Single assignment is at least definately prone to this.

Artefact of low-level semantics

Not really - this problem only exists in low-level languages like C++ that make unboxing visible in their semantics. I don't know any higher-level language that exhibits such behaviour, because object assignment is just replacing a reference there (or has to behave as if).

I should have been more explicit.

I should have been more explicit. I was referring to dataangel's last code example, that's classic slicing which C++/Java/C#/VB.NET etc. allow. It's purely a subtyping issue and can be useful.

In my stance on the covariant data member issue in general, I agree with your post above regarding mutability, they just don't mix.

Slicing is specific to C++

Java does not allow slicing as in C++, because you simply cannot structurally assign objects. And AFAIK, the situation in C# or VB isn't any different either.

Regarding the point that this might be useful: well, yes, but it's an ugly and pretty dangerous hack that can easily be avoided. Note how it might break everything when the parent class is extended later with fields that should not be copied, e.g. some internal object statistics.

It is possible in Java and

It is possible in Java and the others I mentioned. You just have to write the copy constructor or assignment style method yourself because they don't auto generate them like C++ does. So it doesn't happen by accident but it is definately possible.

Object Slicing and Component Design in Java

You have a point that it's dangerous, I've only used it twice and on the first occasion non of the derived classes had any extra state. I'd imagine the vast majority of times it's use is accidental.

So while C++ having auto generated copy c'tors and assignment mixed with value (non-reference) objects makes this a more likely accident than in other languages it's a hazard of subtyping, e.g.

-- Pseudo-code, Haskell-esque + subtyping
data A = { a:Int }
data B = { a:Int, b:Bool }

copyA :: A -> A
copyA(aRec) = { a = aRec.a }

aVal = { a = 1 }
BVal = { a = 13, b = False }

copiedA = copyA aVal
  => { a = 1 }

copiedB = copyA bVal
  => { a = 13 }

You could use a B typed record as an argument to copyA, which is essentially slicing. Although it would arguably be less dangerous in a functional language.

Definition of slicing

It is possible in Java and the others I mentioned.

Well, I didn't say that there is no way to emulate it by hand - of course you can always write a method that just assigns (or constructs) the respective slots by hand. Or you use some form of delegation. But that's the point: you have to be explicit about it, the language doesn't silently replace parts of your object, implicitly, and potentially inconsistently.

So it's not slicing, it's an emulation thereof, with respective limitations (i.e. you have to plan it ahead(*)). Even less I would call your Haskell-esque code an example of slicing - this is just ordinary subtyping.

I would define slicing as an operation that copies or assigns parts of the structure of an object as a whole - without going through ordinary projection and update operations, which might in fact be inaccessible at that point (e.g. because they are private).

(*) In C++ the situation is reversed: you have to plan ahead to preclude it!

So it's not slicing, it's

So it's not slicing, it's an emulation thereof, with respective limitations (i.e. you have to plan it ahead(*)). Even less I would call your Haskell-esque code an example of slicing - this is just ordinary subtyping.

My emphasis.

That's the point I was making, except that the Haskell-esque example is slicing, emulation or no, which I'm arguing is just normal subtyping behaviour. It's implicit/explicit part as you allude that is the key PL design issue in this case.

I do agree wholeheartedly that C++ is a pain in this area and most probably leads to more trouble than it should. Then again C++ is pain in a lot of areas, gotchas like this are its major problem IMO. It's the price for dancing with the devil |:^)

I have to disagree

I made my point clear, so it seems we have to agree to disagree. IMO, built-in vs. encoded makes all the difference (in particular, when the encoding requires non-local measures), otherwise you could as well argue that assembly language has higher-order functions. Cf Felleisen's definition of expressiveness.

[Edit: All we seem to disagree about is the meaning of the word slicing. You are interpreting it in a much broader sense then I am, and I think common usage does. But apart from that we seem to be in violent agreement.]

I agree, to agree to disagree.

Nothing to add really, that posts says it all. Have a good weekend.

Covariant members

IIRC, Eiffel allows you to covariantly redefine members. One issue you must keep in mind is that A may have a method that assigns to x:

class A {
    void SetX(Base const& new_x) { x = new_x; }
protected:
    Base x;
};

You'll run into trouble here if x is of type Derived at runtime because of redefinition in B.