C++ Logo

std-proposals

Advanced search

Re: Conditional final class-virt-specifier

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Wed, 15 Jul 2020 11:02:54 -0400
On Wed, Jul 15, 2020 at 2:11 AM Jason McKesson via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> On Tue, Jul 14, 2020 at 4:56 AM Paweł Benetkiewicz via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
> >
> > Modern C++ compilers use LARL as resolving ambiguity may (and often do)
> require a separate pass. Or many passes. In fact, C++ can require an
> infinite lookahead.
> > Unconditional final remains the same, conditional final may require
> additional pass, same as any other `constraint-logical-or-expression`.
>
> Um, no it doesn't. Constraints of any kind do not require parsing the
> definition of the construct being constrained. Constraints are only
> based on the declaration. You cannot constrain a class template `T` on
> the properties of `T` itself.
>

+1.


> I think the point you missed is that, if `final` is conditional, then
> to answer this question, `Derived` must be fully defined (if the
> condition relies on any property of `Derived` other than that it
> exists). Therefore, if a compile error has not happened by the time
> you're asking this question, then `Derived` has already inherited from
> `Base`.
> So what's the point of asking a question whose answer is *certainly* "yes"?
>

+1.

>
> > It could be also split into two separate type traits like
> is_unconditionally_final and is_conditionally_final, but it could break
> backwards compatibility.
> >
> > > I have never *heard *of "conditional inheritance."
> > Just first few googled examples:
> > -
> https://stackoverflow.com/questions/5040831/restrict-the-classes-that-may-implement-an-interface


// https://godbolt.org/z/6T4vq6
template<class T>
struct Foo {
protected:
    template<class This>
    explicit Foo(This *self) {
        static_assert(std::is_base_of_v<T, This>);
        assert(self == this);
    }
};

struct Bar {};
struct Baz1 : public Bar, public Foo<Bar> { // OK
    explicit Baz1() : Foo(this) {}
};

> -
> https://stackoverflow.com/questions/5767160/allowing-implementing-interface-only-for-specific-classes


Make the constructor private and `friend` only the allowed classes.

> -
> https://stackoverflow.com/questions/26465237/allow-a-mock-class-to-inherit-from-a-final-class


> If the class is final, you *do not need to derive from it*. If you *do* need
to derive from it, *it is not final*. Pick one.


> > -
> https://stackoverflow.com/questions/33007574/restrict-classes-that-can-implement-interface-in-java


// https://godbolt.org/z/TscWzv
struct Iface { virtual ~Iface() = default; };
struct Superclass {};

template<class T>
struct IfaceImpl : Iface, T {
    template<class... Args>
    explicit IfaceImpl(Args&&... args) : T(std::forward<Args>(args)...) {}
};

template<class T, class... Args> requires std::is_base_of_v<Superclass, T>
std::unique_ptr<Iface> make_iface(Args&&... args) {
    return std::make_unique<IfaceImpl<T>>(std::forward<Args>(args)...);
}

struct Class {};
struct Subclass : Superclass {};

int main() {
    auto x = make_iface<Subclass>(); // OK
    auto y = make_iface<Class>(); // ERROR
}

[...]

> Class `final` is already a dubious prospect in C++ because C++ uses
> inheritance for things that other languages do not. C++ uses
> inheritance where other languages might use mixins or partial classes
> for example. C++ users sometimes employ inheritance to gain access to
> the empty base optimization when applicable. C++ users sometimes
> employ inheritance for test driven frameworks (as shown by one of your
> questions).
>
> Basically, declaring a class to be `final` should *always* be looked
> on as a code smell in C++. It makes about as much sense in C++ to
> declare that a class cannot be a base class of another class as it
> does to declare that a class cannot be a *member* of another class.
>

Here I disagree with Jason. The `final` modifier is useful (or at least
harmless) in C++ pretty much exactly whenever classical polymorphism is
useful. They say: "Make every polymorphic class either abstract or final";
I agree with that. Now, it's true that you could mark the leaf class
/*final*/ instead of final and you'd reap pretty much all the same benefits
— the keyword doesn't really *do* anything except documentation — but at
least it doesn't prevent you from doing anything that you should be wanting
to do in the first place. (It prevents you from inheriting from the final
class, which is something you *shouldn't* want to do.)
I agree with your last sentence, btw. If you're using classical
polymorphism, it also does not make sense to declare a class instance as a
*member* of another class.

struct FooFace { virtual void f() = 0; ... };
struct FooImpl : FooFace {
    BarImpl bar; // should almost certainly be `BarFace *bar` for
dependency injection
    ...
};

Classes (like `std::string`) that are intended to be used as members,
value-semantically, should not inherit from anything and should not be
inherited from (except for the Empty Base Class Optimization) and therefore
shouldn't mess around with `final`, or `virtual`, or `protected`, or any of
the other stuff on the "Java" side of C++.


> > Furthermore: the proposal states "Disadvantages: none", "Design
> trade-offs: none". While the latter may end up being somewhat accurate, the
> first one is complete nonsense, rendering this proposal technically
> ill-formed.
> > So will it be ok if I change 'Disadvantages' from "None" to "Proposal
> increases complexity of language by adding optional `final-clause`."? I've
> read a few proposals (both PR… and N…) and I didn't find such sentences.
>
> It's more that, if you're going to have a list of "Disadvantages" or
> "design trade-offs", then it needs to actually list them honestly.
>

By "design tradeoffs," we mean "I considered doing the feature
with syntax/semantics X instead, which would be nice because Y, but I
rejected that alternative design because Z." For example, you chose to mess
with the `is_final` type-trait instead of ignoring the type-traits; why?
You chose to overload `is_final` instead of introducing a new trait; why?
And so on and so forth. (But of course all this is moot at the moment
because the core of your proposal, the whole "use names without declaring
them" bit, doesn't work.)

–Arthur

Received on 2020-07-15 10:06:25