13
$\begingroup$

In C++, you could refer to members of a class directly in its member functions. Many other languages requires an explicit this. or this-> prefix, which is longer to write. I have also seen some projects in C++ that add an m_ prefix to the member variables, implying they find something bad without the explicit prefix.

What are the pros and cons of not requiring this prefix?

$\endgroup$
2
  • $\begingroup$ Is this about C++ or programming languages in general? In C++, there are also places around templates where parsing strictly requires it just because C++ templates are a bit strange in that regard. $\endgroup$
    – feldentm
    Jul 21, 2023 at 8:42
  • $\begingroup$ @feldentm It's a question in general. But if there is a situation related templates, that makes it difficult to use implicit this, for valid reasons that is not because C++ is just designed as so, it may also apply to other languages with templates. So it is likely a valid con. $\endgroup$
    – user23013
    Jul 21, 2023 at 8:47

7 Answers 7

11
$\begingroup$

Con: Aliasing between instance and local variables

Especially for constructors, you often need both variables and instance method with the same names:

Klass(int version, string name, bool isAlive) {
    this.version = version;
    this.name =  name;
    this.isAlive = isAlive;
}

In this case you could not use the prefix syntax. People often use slightly different names like a m_ prefix so they can use the shorter syntax.

It would be more consistent if you could use the same syntax everywhere.

Con: Inability to see what has temporary or permanent effect at a glance.

Suppose you are reading a long method and it looks like this:

                      foreach (Item item in allItems) {
                           if (item.property = filter) {
                                goodItems.push(item)
                           }
                      }

You can't tell from this bit of code if allItems, filter or goodItems is just a temporary variable or stored in the instance.

To make things worse there could be a goodItems in the instance but this is just a local variable with the same name.

Assigning to instance attributes are side effects, one of the more important things to look for when trying to understand a function. Giving them a this. prefix makes it easier to identify these side effects.

A naming convention that separates locals from properties goes a long way to solve both issues.

$\endgroup$
4
  • 1
    $\begingroup$ I also included that first one in my answer, oh well. I'll note that neither of these apply to C# which uses a different naming convention for locals (camelCase) vs properties (PascalCase). $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 11:44
  • 1
    $\begingroup$ The first one is not correct with optional this nothing is permanently hidden. The second one assumes no semantic syntax highlighting, which is a common feature these days. Those are still valid points explaining dates style guides. $\endgroup$
    – feldentm
    Jul 20, 2023 at 17:54
  • 1
    $\begingroup$ You can't tell from this bit of code if allItems, filter or goodItems is just a temporary variable or stored in the instance -- maybe not if presented like this, but I use an IDE that's smart enough to give instance variables a different colour to others, and the existence of this feature kind-of negates this point for most scenarios, I think. $\endgroup$
    – occipita
    Jul 20, 2023 at 23:07
  • 1
    $\begingroup$ C++ mitigates the first issue using initialization lists: Klass(int version, string name, bool isAlive): version(version), name(name), isAlive(isAlive) {} works seamlessly because in each context where the identifiers appear, exactly one of the meanings is valid. It's presumably not the easiest thing to parse, but C++ is overall not easy to parse. $\endgroup$ Jul 21, 2023 at 5:27
8
$\begingroup$

I have a couple arguments for requiring it.

Longer compile times

Allowing implicit this/self means there's more places the compiler has to look to see whether the variable is in scope or not, and if so what its type is. This may seem like it's not that big of an issue, but I've run into Swift code where the type checker would time out if you used implicit self rather than saying it explicitly.

Sometimes you need it anyways

Consider this common pattern in Swift:

public struct Example {
  public var foo: Int
  public var bar: Int

  public init(foo: Int, bar: Int) {
    self.foo = foo
    self.bar = bar
  }
}

Swift allows you to leave self implicit, but without arbitrarily changing the names of the parameters to the init, there's no way to leave it implicit here. Note that the foo: and bar: here specify both the internal name and the external label; to change only the internal name would be

public struct Example {
  public var foo: Int
  public var bar: Int

  public init(foo _foo: Int, bar _bar: Int) {
    foo = _foo
    bar = _bar
  }
}

which is more effort than it's worth.

Extension methods

This one depends on the way you write extension methods. It doesn't apply to Swift- or Rust-style implementation blocks, but it does apply to C# extension methods.

Implicit this can't be used in extension methods in C#. Consider the following code:

public class Example
{
  public int X { get; set; }

  public Example(int x)
  {
    X = x;
  }

  public int Add1(Example other)
  {
    return X + other.X;
  }
}

public static class ExampleExtensions
{
  public static int Add2(this Example first, Example second)
  {
    return first.X + second.X;
  }
}
$\endgroup$
12
  • 4
    $\begingroup$ I highly doubt that the difference in compile time will be noticable $\endgroup$
    – mousetail
    Jul 19, 2023 at 11:46
  • 1
    $\begingroup$ @mousetail Feel free to believe that, but Swift's type checker disagrees, evidently. $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 11:47
  • $\begingroup$ One thing to add to extension methods is, you could omit this for Add1() in methods of Example, but not for Add2(), which is a surprise, but makes sense if they are trying to prevent code being broken by random extensions. $\endgroup$
    – user23013
    Jul 19, 2023 at 12:01
  • 1
    $\begingroup$ @MatthieuM. There's a difference in how Swift/Rust and C# handle extensions. In C#, foo.Add2(bar) is just syntactic sugar for ExampleExtensions.Add2(foo, bar). In Swift, extensions can also be used to add conditional or retroactive protocol conformances, so an extension block may generate a new vtable. Rust is much the same as Swift, except that impl blocks are the only way (modulo macros) to implement traits. $\endgroup$
    – Bbrk24
    Jul 20, 2023 at 13:43
  • 1
    $\begingroup$ Swift type checking times are probably a bit of a red herring for other languages. Swift uses a global type inference algorithm that could cause it to search in a lot of possible matches if an identifier isn't explicitly scoped. This results in notoriously odd performance issues when compiling code that seems perfectly fine (example). $\endgroup$
    – occipita
    Jul 20, 2023 at 23:20
4
$\begingroup$

Because, currently, all answers are cons. Let me try some pros. Obviously, there must be some if the feature is so common.

Pro: Brevity

The code contains less words and characters and is, thus, easier to read. In OOP languages, methods could easily require prefixing this before every data access which clearly won't help readability. Also, having them in a slightly different color then static entities is much faster to get then parsing "this." as a human. Unless you are colorblind of course.

Pro: Correct Abstraction

Implicit this is a common feature in OOP languages. There, it is common to abstract behavior by data structures. The functions operating on them have the fields of their associated data structure at hand. Hence, it makes sense to allow accessing them like local variables.

$\endgroup$
3
$\begingroup$

It goes further than that. Another thing that C++ does here is to fix the type of this to be a pointer to the class type that the member function is a member of. So in:

class Foo {
    void bar() {
        // type of this is always Foo*
    }
};

You might think that this is obvious, but it is more restrictive than you think. There are cases where you want this to be another type. For example, if you have inheritance, you might want the base class to do something with the derived class, without requiring virtual functions:

struct Base {
    void do_something_with_derived() {
        this->foo();
    }
};

struct Derived: Base {
    void foo(); // non-virtual!
};

Derived bar;
bar.do_something_with_derived(); // works if this is Derived*

Another issue that plagues C++ is that member functions can have cvref qualifications, allowing you to make overloads for const, volatile and reference-qualified versions of an object of the class. For example, you want operator[] to return a const reference if the object is const and a non-const reference if the object is non-const. That means you have to make two overloads:

class MyIntArray {
    int data[…];
public:
    int& operator[](std::size_t index) { return data[index]; }
    const int& operator[](std::size_t index) const { return data[index]; }
};

Notice how the body of those two functions is identical. This is a simple function, but for a more complex function you really want to avoid the code duplication. If the type of this is not determined by the function declaration itself, but by the type of object it is applied on, you could then create a templated version of operator[]() that deduces everything for you. This has been made possible in C++23 using the explicit object parameter:

class MyIntArray {
    int data[…];
public:
    auto& operator[](this auto&& self, std::size_t index) {
        return self.data[index];
    }
};
$\endgroup$
6
  • 2
    $\begingroup$ While it's true that this could have more detailed types, notably in inheritable clone functions which may require this in a type in the template argument, I don't think it has anything to do with implicit or explicit this, as the implicit this usually shares the same type as the explicit one. If there is a case that it would be helpful to access this in different types by using different calls in the same function, it may have a point. $\endgroup$
    – user23013
    Jul 19, 2023 at 16:52
  • $\begingroup$ You give examples where explicit this is helpful, but no one is arguing that explicit this shouldn't be allowed; the question is whether it should be mandatory in every case. $\endgroup$
    – benrg
    Jul 20, 2023 at 0:44
  • 1
    $\begingroup$ Maybe this should be a new question then? Pros and cons of having a this with a non-fixed type? $\endgroup$
    – G. Sliepen
    Jul 20, 2023 at 7:02
  • 1
    $\begingroup$ This answer should be deleted and moved to another question on explicit this type as it is not related to the question here. The explicit this type is also interesting and has completely different points against and in favor. $\endgroup$
    – feldentm
    Jul 20, 2023 at 18:09
  • 1
    $\begingroup$ Semantically, it would make much more sense in C++ for this to be a reference rather than a pointer. However, it's a pointer because the this keyword was added to the language before references were, and it can't be fixed because of backwards compatibility concerns. $\endgroup$ Jul 21, 2023 at 5:30
1
$\begingroup$

Pro: Unification with closure functions

Many languages have the feature called closure, that makes it possible to define a function inside another function, and use the variables in the other function. It is often used to write callback functions passed to other functions, which is usually short, and it usually makes sense to read them in the same context. So it is generally allowed to use variables in the outer scope directly.

If someone is extreme about unifying everything, they may want classes not looking too different from functions. It might become more confusing using the different ways, if someone writes a method function inside a class inside another function.

Seeing how many cons there are, this is probably not the right way. But it isn't obvious what a good alternative is like.

$\endgroup$
1
$\begingroup$

Telling apart instance, local, and global vars

In a statically-typed language such as Java or C++, we know ahead of time which variables are local variables, global variables, or instance variables. In dynamic languages this can be more difficult because they often allow the programmer to create new instance fields at run-time.

Many dynamic languages variables are global-by-default. Assigning to a variable that isn't in scope as a local variable will assign to a global variable. For example, in Javascript:

x = 10      # global variable
this.x = 20 # instance variable

In Python, variables are local-by-default. If you assign to a name that's not already a local variable it will create a new local variable. We have to use self. to assign to instance variables, and a global annotation to assign to global variables.

x = 10      # local variable
self.x = 10 # instance variable.

Some dynamic languages may choose to avoid the need for this by using dynamic scoping for all variables. The variable always checks the local, then instance, then global scope, in that order. The downside here is that in a dynamic language this scope may change at run-time if someone assigns a new field to the instance variable. This is can cause variables to unexpectedly change scope. It is also worse for performance, because the language doesn't know at compile-time where to look for the variable.

$\endgroup$
-2
$\begingroup$

Con: No computation on constructors before allocation

public class Example
{
    public Example( int arg )
    {
        if ( arg > MAX )
            throw new OutOfBoundsException(nameof(arg));
        this = new(); // Lots of very heavy properties
    }
}

Con: No polymorphic super type instantiation

public class Channel : ChannelInterface
{
    public Channel( int capacity = 0 )
    {
        if ( capacity > 0 )
            this = new BoundedChannel( capacity );
        else
            this = new UnboundedChannel();
    }
}

$\endgroup$
6
  • 2
    $\begingroup$ I don't understand what you mean by "No computation on constructors" $\endgroup$
    – mousetail
    Jul 19, 2023 at 13:47
  • 7
    $\begingroup$ This has nothing to do with this = object. This is about this.foo = bar as opposed to just foo = bar (with implicit this.). $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 14:14
  • 1
    $\begingroup$ Constructors are methods, that implicit defines this. This answer goes a step further, by explicit requesting this allocation before explicit using this to open new functionality that otherwise is not possible with implicit defined implicit imported this. $\endgroup$ Jul 19, 2023 at 19:29
  • 1
    $\begingroup$ Swift, which has implicit self, also lets you write self = in struct initializers (though not class initializers, for various reasons). $\endgroup$
    – Bbrk24
    Jul 19, 2023 at 19:49
  • 2
    $\begingroup$ Both of these are consequences of precise details of the mechanism used to implement and allocate objects, not the choice of whether or not to require an explicit this reference when referring to members, so seem to not be answers to this question. $\endgroup$
    – occipita
    Jul 20, 2023 at 23:47

You must log in to answer this question.

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