3
$\begingroup$

In the class definitions of an object oriented language, this refers to the object instance the code is about to work upon. Naturally, it should have the same type as the class to be defined. But there are variations of a type that could also be applied to this. self is sometimes an alternative name to this, but sometimes could also refer to the class itself, instead of the object.

To demonstrate, a simple example in C++ would be:

class T {
public:
    int f() const {
        // "this" is in (const T*), instead of just (T*)
    }
}

The answer may seem obvious at first glance: For every variation that could be applied to the parameters, it could also be applied to this. If parameters could be a constant, this could be constant. If parameters could be a rvalue reference, this could be defined as a rvalue reference. (There could be another question for that if anyone is interested.) Object oriented programming works like syntactic sugar over the traditional procedural programming in many cases, after all.

But there are also situations that this logic couldn't be applied easily. Notably, the curiously recurring template pattern (CRTP) refers to something like this:

class Derived : public Base<Derived>

It is implied that code in Base may need to access the type Derived somewhere. Derived is something like the real type of this. CRTP has the obvious disadvantages such as being less intuitive, and making casts between base classes and subclasses difficult. It might be better if we turn it into another variation of this.

But template and generics are too general, that could be used as catch-all solutions to implement many different things, even if it is not optimal. Having CRTP may even make people less likely to report their use case, which may not be covered in a specific variation-of-this solution.

So, what are the variations to the type of this or self that should be supported? I.e. what useful differences could be made to this or self, from just an object of the class to be defined, or just the class, using different declarations of a block of code?

CRTP could be a source of many different variations. But there might be also cases not covered by CRTP.

Note

  • This is not about whether this should be a pointer or a reference, or implicit, as they are almost equivalent. I'm more interested in things that could not be done in other types of this. I.e. It's objectively a valid answer if something could not be done, or could not be done safely in languages without one kind of variation. Say if we have a language like C++ that doesn't support the const qualifier in the example, calling a method in a constant object would be either impossible, or unsafe as it would use the non-const this in the method. So const is a valid answer, although trivial and already widely known.
  • This is not a survey about how things are done in every language. It's about what are the use cases to be covered, or how problems are solved if there are multiple ways.
  • The very traditional things that are applied to any parameters, such as const and rvalue reference, should be better in a separate question, if there is. But they could be referenced if there are some overlaps.
$\endgroup$
13
  • $\begingroup$ I'm posting this question so that information related to this answer has somewhere to go. But I'm not sure the answer itself could be moved here. $\endgroup$
    – user23013
    Jul 21, 2023 at 9:07
  • 1
    $\begingroup$ You've set out some things that this question isn't, but I'm not sure I can tell precisely what it is about. It does feel very broad, at least. $\endgroup$
    – Michael Homer
    Jul 21, 2023 at 9:07
  • $\begingroup$ @MichaelHomer Is it clearer? I think this is much more restrictive than the question devised in the comment of that answer: "Pros and cons of having a this with a non-fixed type?" $\endgroup$
    – user23013
    Jul 21, 2023 at 9:30
  • 1
    $\begingroup$ It seems both very embedded in the C++ milieu and very unconstrained: "what are some things that you could do with this?". It explicitly rules out substantiating evidence, and requiring strong uniqueness between answers doesn't seem to fit the Q+A model here. I'm not sure we're equipped to deal with this question as it's currently set out, and I'm not sure what answer it expects except the reference one - but as you say, that wouldn't fit the question either. $\endgroup$
    – Michael Homer
    Jul 21, 2023 at 9:48
  • 1
    $\begingroup$ "I prefer calling it statically typed, instead of C++ milieu." The issues you're describing don't seem to make sense inherently in statically typed languages considered generally. Quite a few of them don't even support generic types, never mind the ability to implement something like CRTP. $\endgroup$ Jul 27, 2023 at 21:52

2 Answers 2

6
$\begingroup$

Swift's covariant Self

Expanding on @kaya3's answer, Swift has a special Self type that behaves differently depending on where it's used.

Within a struct, enum, or final class, Self has an obvious, fixed meaning, since these types cannot be derived from.

Within a protocol, Self means "the type implementing this protocol". Implementers fill it in accordingly:

protocol AdditiveArithmetic {
  static func + (lhs: Self, rhs: Self) -> Self
}

extension Int: AdditiveArithmetic {
  static func + (lhs: Int, rhs: Int) -> Int { ... }
}

extension UInt: AdditiveArithmetic {
  static func + (lhs: UInt, rhs: UInt) -> UInt { ... }
}

// etc

These first two usages are different from TypeScript's this type, which always represents the value of this and never something else of the same type.

Within a non-final class, Self is more complicated.

In covariant position, such as a function return type, Self is only allowed if the value is self or another expression of type Self. This is the same rule as in TypeScript.

class C {
  func foo() -> Self {
    self // okay
  }

  func bar() -> Self {
    foo() // okay
  }

  func baz() -> Self {
    C() // not okay
  }
}

Note that this becomes complicated in static methods, since you don't have access to a self. Swift deals with this using required init:

class C1 {
  required init(foo: Void) {}
  init(bar: Void) {}

  static func foo() -> Self {
    .init(foo: ()) // okay
  }

  static func bar() -> Self {
    .init(bar: ()) // error
    // Because init(bar:) isn't required, subclasses might not implement it.
  }
}

class C2: C1  {
  required init(foo: Void) {
    // Because init(foo:) is required, you must override it.
    super.init(foo: ())
  }
}

In contravariant position, such as an argument, it is forbidden. You are required to replace it with the class name:

class C {
  func foo(_ other: Self) {}
  // error: covariant 'Self' or 'Self?' can only appear as the type of a property, subscript or method result; did you mean 'C'?
}

This combines weirdly with the protocol rule above, due to the Liskov substitution principle:

protocol P {
  func foo(_ other: Self)
}

protocol Q {
  func bar(_ other: Self)
}

class C1 {
  func foo(_ other: C1) {}
}

class C2: C1, Q {
  override func foo(_ other: C1) {} // This 'Self' becomes C1...
  func bar(_ other: C2) {} // ...but this 'Self' becomes C2...
}

let x = C1(), y = C2()

(y as C1).foo(x) // ...because this is legal...
(y as C1).bar(x) // ...but this isn't.

In invariant position, such as inout arguments to functions, Self is just illegal.

protocol Incrementable {
  static func += (lhs: inout Self, rhs: Self)
}

class C: Incrementable {
  static func += (lhs: inout Self, rhs: C) {}
  // error: protocol 'Incrementable' requirement '+=' cannot be satisfied by a non-final class ('C') because it uses 'Self' in a non-parameter, non-result type position
}

I'd say that referring to invariant position as "non-parameter, non-result type position" is a bit misleading: lhs here isn't neither, it's both.

$\endgroup$
4
$\begingroup$

This isn't a complete answer to the question, but a similar issue is common when methods are designed for chaining. For example, in Java:

class Foo {
    int x = 0;
    Foo incrementX() {
        x++;
        return this;
    }
    Foo printX() {
        System.out.println(x);
        return this;
    }
}

That is, the methods are supposed to return the same instance, to allow for chains like foo.incrementX().printX(). The problem comes when a subclass is defined:

class Bar extends Foo {
    int y = 0;
    Bar incrementY() {
        y++;
        return this;
    }
}

Bar inherits the methods from Foo, but since their return type is Foo, it is not possible to chain methods like bar.incrementX().incrementY(). Hence the need for the "curiously repeating template pattern" in languages like C++ and Java.

On the other hand, Typescript has an elegant solution: this can be used as a type, to mean "the type of the expression this", i.e. the type of the subject of the method call. In Typescript, the above example could be written like

class Foo {
    x: number = 0;
    incrementX(): this { ... }
    printX(): this { ... }
}
class Bar extends Foo {
    y: number = 0;
    incrementY(): this { ... }
}

And now the chain bar.incrementX().incrementY() is valid, because the return type of incrementX() is resolved to be the type of the method subject, bar. Typescript calls this type the "polymorphic this type".

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .