Stannum

Useless language features are useless

2019-03-01

It strikes me how language designers don’t get this simple truth:

If the feature usefulness does not outweigh the complexity it adds to the language, it shall be left out.

In particular, if the feature is of the ‘no-op’ kind—i.e. it can be removed while retaining an identically functioning program without significantly sacrificing code clarity, or there is another way to express the same idea within the language in perhaps even better way. Many such features are better to be turned into implementation specific annotations (like C++ attributes), compiler warnings, or left out completely.

Let’s take a look at how it applies to C++ .

Access specifiers

C++ has three access specifiers: public, private and protected. They are the glorified mechanism for encapsulation in C++. However, if you ever wrote C++ code before, you know how poor the encapsulation they provide:

  • Defining a private member of a given type in a header requires including the definition of that type too. By including the header files for the private types in our library distribution we are leaking the implementation details, while inflating the compile times on the way.
  • Exposing private members like that has also negative implication on the stability of the ABI.

If we remove the access specifiers from the language, making everything public instead, valid programs would still be valid.[1] Moreover, C++ has other encapsulation mechanisms that avoid the aforementioned problems, but that’s a topic for another day.

The final specifier

This is a great example of a no-op feature. The only purpose of the final specifier is to revoke the freedoms of inheriting from the class or overriding some of its virtual members.

Is it useful? No.

Yet, since its introduction to C++, some coders mark everything they can as final, without a reason, of which there can’t be any because there are none.[2]

You can’t foresee the infinity of the scenarios where inheriting your class would be useful. It’s straight against the principles of object orient design to bar me from overriding a function, even just to log the call.

For optimization purposes the code of the virtual function can be extracted into a common method:

struct A : B {
	void f_concrete() {}
	virtual void f() { f_concrete(); }
	virtual void g() { f_concrete(); } // instead of calling f directly
};

Though it’s a rare case of optimization where this would be needed.

The override specifier

This one is a strong candidate for elimination, as it does match the criteria set above. It doesn’t add any functionality to the language, but rather helps to prevent bugs when the overridden function signature doesn’t match any of the base class functions.

There are various alternatives to the override specifier. One would be to issue a warning when there are multiple virtual functions of the same name,[3] as this is already known to be a bad practice. Such a warning could be silenced by an optional override attribute. The benefits of this approach is that it automatically applies to legacy code, or catches the case when the programmer forgets to write override.

Alternatively, a better feature could have been introduced into the language: when overriding a virtual function one would need to explicitly state the name of the base class that the function is overriding. For example:

struct A { virtual void f() {} };
struct B { virtual void f() {} };
struct C : A, B {
	virtual void A.f() {}
};

As the example above shows, that would also solve the problem with the ambiguity arising in multiple inheritance. However, that too, is an entire topic for another day.

On a related note, it should have been an error to override a virtual function without using the virtual specifier.

The restrict qualifier

This one is part of C, but multiple compilers support it for C++ as an extension. It’s purely an optimization hint though, so there’s no reason for it to be a first-class language feature. Its place is with other optimization hints—as an optionally supported attribute.

The const qualifier

The topic of constness in C++ is rather controversial. It is never clear what does it mean for an object to be const, in particularly in relation to the contained or pointed objects, external resources (like files, databases, etc), or mutable members. On top of that the constness can rarely be enforced at run-time.

The way const is implemented in the language causes duplicate code at times. E.g. a container needs two versions of almost every accessor: one for const container returning const references and iterators, and one for non-const container returning non-const references and iterators.

Since C++11 the constexpr specifier was introduced on top. I argue that that const should have been more like what constexpr is now. In particular we want marking a method as const to double as both, const and non-const method.

However, specifying how all this would work, together with compile time evaluation[4], is not a trivial feat.

Footnotes

  1. There are a few exception to this. First the memory layout of the object might change, second the private inheritance becoming public might change the behavior of dynamic_casts. This reinforces the point made though, as the feature in question complicates the language beyond the perceived benefits.
  2. I genuinely tried to find some on the web, e.g. In C++, when should I use final in virtual method declaration?. Unfortunately those examples are artificially constructed and aren’t very convincing.
  3. Admittedly, this won’t trigger when there’s a typo in the function name.
  4. C++ can evaluate at compile time through templates and constexpr. I envision a properly designed language in which the two are rather merged, with compile time reflection added to the brew.

Share on

See also