hsutter / cppfront

A personal experimental C++ Syntax 2 -> Syntax 1 compiler
Other
5.48k stars 242 forks source link

[BUG] Mutable member functions shouldn't be constant #266

Closed msadeqhe closed 1 year ago

msadeqhe commented 1 year ago

Describe the bug All member functions are constant even if they change the class state (e.g. member variables).

To Reproduce This is the sample CPP2 code:

klass :type = {
    a :string = "HELLO WORLD!";
    constantCall :(this) = { return; }
    mutableCall :(this) = { this.a = "OOPE!"; }
}

main :() = { return; }

I've used Compiler Explorer.

It should generate the following C++ code:

class klass  {
    private: string a {"HELLO WORLD!"};
    public: auto constantCall() const -> void{return ; }
    public: auto mutableCall() -> void{(*this).a = "OOPE!";}
};

auto main() -> int{return ; }

But it generates the following C++ code:

class klass  {
    private: string a {"HELLO WORLD!"};
    public: auto constantCall() const -> void{return ; }
    public: auto mutableCall() const -> void{(*this).a = "OOPE!";}
};

auto main() -> int{return ; }

Additional context function mutableCall shouldn't be const because it changes the class state (e.g. member variables).

AbhinavK00 commented 1 year ago

You should mark the this parameter as inout to express your intent of mutating the class. So the member function definition will look like this:

mutableCall : (inout this) = { this.a = "OOPE!"; }
msadeqhe commented 1 year ago

Thanks. You're right, and it works as intended now.

By the way CPP2 should output a compilation error message.

AbhinavK00 commented 1 year ago

Yes, a compilation message should be emitted. I guess it's because of the reason that Herb is not done implementing classes yet. Or is he? 👀

filipsajdak commented 1 year ago

Currently, classes still need to be finished. Herb wrote in the commit (https://github.com/hsutter/cppfront/commit/258190ef6df8db8bc31d0eb5d94094da5d800623) message:

Added initial basic support.

Includes:
- access-specifiers: public, protected, private
- defaults: in type scope (aka members), types and functions are public by default, objects (data members) are private by default
- this-specifiers: implicit, virtual, override, final (but can't use them much since we don't have inheritance syntax yet, see next list)
- explicit `this` parameters (where the this-specifiers go)

NOT included yet:
- special member functions (`operator=`)
- inheritance / base classes
- metaclass functions to apply checks, defaults, and generated code (that'll be last...)

I have been playing with it, and I explored how to use it:

N : namespace = {
    t : type = {
        i : int;
        public j : int = 1;
        protected k : std::pair<int,int> = (1,2);

        f0: () = {
            std::cout << "f0" << std::endl;
        }
        private f1: (this) = {
            std::cout << "f1" << std::endl;
        }
        protected f2: (virtual this) = {
            std::cout << "f2" << std::endl;
        }
        // f3: (override this) = {
        //     std::cout << "f3" << std::endl;
        // }
        f4: (implicit this) = {
            std::cout << "f4" << std::endl;
        }
        // f5: (final this) = {
        //     std::cout << "f5" << std::endl;
        // }
        f6: (in this) = {
            std::cout << "f6" << std::endl;
        }
        f7: (inout this) = {
            std::cout << "f7" << std::endl;
        }
        // f8: (out this) = {
        //     std::cout << "f8" << std::endl;
        // }
        f9: (move this) = {
            std::cout << "f9" << std::endl;
        }
    }
}

Generates:

namespace N    {
    class t   {
        private: int i; 
        public: int j {1}; 
        protected: std::pair<int,int> k {1, 2}; 

        public: static auto f0() -> void{
            std::cout << "f0" << std::endl;
        }
        private: auto f1() const -> void{
            std::cout << "f1" << std::endl;
        }
        protected: virtual auto f2() const -> void{
            std::cout << "f2" << std::endl;
        }
        // f3: (override this) = {
        //     std::cout << "f3" << std::endl;
        // }
        public: auto f4() const -> void{
            std::cout << "f4" << std::endl;
        }
        // f5: (final this) = {
        //     std::cout << "f5" << std::endl;
        // }
        public: auto f6() const -> void{
            std::cout << "f6" << std::endl;
        }
        public: auto f7() -> void{
            std::cout << "f7" << std::endl;
        }
        // f8: (out this) = {
        //     std::cout << "f8" << std::endl;
        // }
        public: auto f9() && -> void{
            std::cout << "f9" << std::endl;
        }
    };
};

Commented sections are the ones that do not work yet.

msadeqhe commented 1 year ago

We have cv-qualifiers and ref-qualifiers for member functions in C++, instead we have in, inout, out, forward, move and copy in CPP2.

In C++ the following combinations of qualifiers are available (ignore volatile), and according to function parameters in CPP2, I write the corresponding this parameter for them in CPP2:

class X {
    auto func_without_qualifiers() { ... } // copy this
    auto func_ref()              & { ... } // inout this
    auto func_rref()            && { ... } // move this
    auto func_const()        const { ... } // copy this
    auto func_ref_const()   const& { ... } // in this
    auto func_rref_const() const&& { ... } // ???
};

But your test examples have different results, does CPP2 preserve overload resolution semantic of C++?

filipsajdak commented 1 year ago

@msadeqhe in P0708 Parameter passing section 1.7 Segue: The this parameter Herb describes the following:

For member functions, the this parameter is implicit and so can’t be qualified using the same syntax as other parameters, and so the C++ convention is to express qualifiers on the this parameter via special additional grammar at the end of the member function parameter list. Declarative parameter passing for this would then go in the same place as all the other this qualifiers:

// qualifying “this” on member functions

auto f() in {}.     // present me an X I can read from
auto f() inout {}.  // present me an X I can read from and will write to
auto f() out {}.    // present me an X I will assign to
auto f() move {}.   // present me an X I will move from
auto f() forward {} // present me an X I will pass along

Note how these correspond to, and subsume and extend, the existing member function qualifications:

Passing this object... Similar to today's... Notes
auto X::f() in auto X::f() const in additionally passes *this by value when that is cheaper
auto X::f() inout auto X::f() Today’s existing “mutable” default
auto X::f() out X::X (constructor) See §2
auto X::f() move auto X::f() && move additionally guarantees that if the function returns normally, this object is known to be moved-from
auto X::f() forward n/a Today there is no way to forward this object

The syntax is not the same but the meaning is similar.

I don't know what implicit keyword means. @hsutter can you clarify?

hsutter commented 1 year ago

Commented sections are the ones that do not work yet.

Note that override and final do "work" in that they emit the correct Cpp1 code, but because I haven't implemented inheritance yet you can't actually legally exercise them. (Note that final implies override.)

And out this on a non-operator= function is disabled because the only way for it to be useful would be to allow calling the function on an uninitialized object. I may allow that extension, but it seems a bit novel and I want to see actual motivating use cases down the road before spending time allowing and teaching it.

I don't know what implicit keyword means. @hsutter can you clarify?

It's the opposite of explicit. Constructors will be explicit by default, and implicit is the opt-in to allow implicit conversions to this type.

Note: The next commit will have more checking, to allow implicit only on an out this parameter of an operator= function (a constructor).

filipsajdak commented 1 year ago

Thank you for the explanation! implicit is a clear and probably long-awaited feature.

I wasn't precise about override and final - they do work on the cppfront side, but as there is no inheritance yet, we can't override a virtual method from the base class, and we cannot make it final.

hsutter commented 1 year ago

@msadeqhe Also note that the intentional parameter passing in Cpp2 doesn't have to cover all the options Cpp1 allows today, because today's approach is low-level and allows combinations that don't make sense. You had one such example in your code:

auto func_rref_const() const&& { ... } // ???

Right, that's similar to an ordinary parameter like

auto func_rref_const( X const&& x ) { ... } // ???

and in both cases the combination doesn't make sense: const means you can't modify the argument, but && means it only accepts rvalues, and the only reason to accept only rvalues is to move from the argument... which is typically a modifying operation (except for types like int).

In Cpp1 this comes up (and we have to teach why it doesn't make sense) because the low-level "how to pass a parameter" mechanical detail-oriented approach allows all the combinations, not only the ones that make sense. This is something that naturally disappears when you have a high-level "what I will use the parameter for" declarative usage approach, which also eliminates most need to overload them.

Cpp1 has a few of these things that I aim to eliminate... const&& parameters is one, and protected inheritance is another, where the constructs are legal but people haven't been able to find realistic uses for them. (I spent a long time in the 1990s trying to come up with a use case for protected inheritance, and one time I thought I found one but the next day I had to admit it was really contrived and gave it up.) My current plan for Cpp2 is to allow only public inheritance, and spell it as is instead of public... bring on the pitchforks and lederhosen! ... but if there's only IS-A public inheritance then I can stop teaching (a) why not to use private inheritance, (b) why protected inheritance has no uses, and (c) why public inheritance should always model IS-A (if Cpp2 spells it is, it'll be right there in the keyword and not need a book to remind people that's what it should model).

AbhinavK00 commented 1 year ago

And out this on a non-operator= function is disabled because the only way for it to be useful would be to allow calling the function on an uninitialized object. I may allow that extension, but it seems a bit novel and I want to see actual motivating use cases down the road before spending time allowing and teaching it.

So, are we not getting named contructors? 😕

Edit: Referring to the named contructors as in parameter passing paper.

SebastianTroy commented 1 year ago

I like that construction and assignment will be unified via the = operator, can you just use the factory pattern for "named" constructors?

On 9 March 2023 19:14:03 Abhinav00 @.***> wrote:

And out this on a non-operator= function is disabled because the only way for it to be useful would be to allow calling the function on an uninitialized object. I may allow that extension, but it seems a bit novel and I want to see actual motivating use cases down the road before spending time allowing and teaching it.

So, are we not getting named contructors? 😕

— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/266#issuecomment-1462631281, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQLDOSJNGR6L7SA3V23W3ITXRANCNFSM6AAAAAAVSHRC2U. You are receiving this because you are subscribed to this thread.Message ID: @.***>

hsutter commented 1 year ago

So, are we not getting named contructors? 😕

Good news: In cppfront today you effectively already have named constructors throughout the language 😁 because any function with an out parameter is a delegating constructor(*) when you pass an uninitialized argument to that parameter. For example, you can invoke f(out x: string) with the call f(out mystring) where mystring is an uninitialized variable, and f really will construct mystring. I explain this more in this 1-min clip

The main difference I mean above is that if I were to allow a non-operator= member function to have an out this parameter specifically, say a member function f(out this), then I could (but don't yet) allow out also on the front of the function call, i.e., a syntax like out myobject.f(). Because a member function has access to the class's private data, such a member function could potentially also choose how to directly construct the individual members of the class (whereas other out parameters can construct the object but only using the provided constructors). Other than that granularity, though, cppfront already allows any function with an out parameter to act as a delegating constructor.(*)


(*) I say "delegating constructor" a couple of times there just to emphasize that already a Cpp2 function with an out parameter is a constructor, but one that delegates to one of the class-provided constructors (it doesn't have access to the private members to fully customize construction to arbitrary states, which is a Good Thing because that would break encapsulation and the ability for a type to control its own invariant). That makes out parameters be equivalent to granting the ability to write named constructors that delegate to other class-provided constructors, which is essentially the C++11 delegating constructor feature%0A%7D%3B) generalized throughout the language to all functions.

AbhinavK00 commented 1 year ago

Thanks for clarifying!! One other question I had was under which parameter passing category would something like std::vector::reserve fall? And also, can an issue be opened to discuss callsite marking of function parameter to discuss different alternatives?

hsutter commented 1 year ago

Sure thing, quick ack:

which parameter passing category would something like std::vector::reserve fall?

That's a non-const function, so it would be reserve: (this, new_capacity: size_type).

can an issue be opened to discuss callsite marking of function parameter to discuss different alternatives?

This has come up in a couple of issue, see #198 for example which summarizes the state of this for the time being. There's also some discussion in the d0708 paper.

AbhinavK00 commented 1 year ago

That's a non-const function, so it would be reserve: (this, new_capacity: size_type)

Why is this parameter not inout? And even if it was inout, reserve invalidates the iterators and any references, so I guess it should be move? I'm not sure. And it shouldn't be in because the vector may be unitialised and definitely not out as it does not initialise any unitialised vector passed to it.

This has come up in a couple of issue,

Yes, that's why maybe one issue where we can discuss would be better instead of same thing scattered across various (closed) issues. I'll also mention all the issues it has come up in if I open a new issue.