Skip to content

[SUGGESTION] [BUG] The syntax of assignments and initializations should be the same for multiple arguments. #449

Closed
@msadeqhe

Description

@msadeqhe

Describe it

I don't know I'm reportring a bug or a feature request. This is an example:

point: type = {
    operator=: (out this, x: int, y: int) = {}
}

check: (p: point) = {}

main: () = {
    p0: point = (0, 0);
    p0 = (0, 0); // BUG!
    // Use this instead:
    p0 = : point = (0, 0);

    p1: point;
    p1 = (0, 0);
    p1 = (0, 0); // BUG!
    // Use this instead:
    p1 = : point = (0, 0);

    check((0, 0)); // BUG!
    // Use this instead:
    check(: point = (0, 0));
}

I've highlighted important lines with BUG! comment.

What is the result?

cppfront generates the following wrong Cpp1 code for main:

auto main() -> int{
    point p0 {0, 0}; 
    p0 = 0, 0;   // BUG!
    // Use this instead:
    p0 = point{0, 0};

    cpp2::deferred_init<point> p1; 
    p1.construct(0, 0);
    p1.value() = 0, 0;// BUG!
    // Use this instead:
    p1.value() = point{0, 0};

    check((0, 0)); // BUG!
    // Use this instead:
    check(point{0, 0});
}

So p0 and p1 don't work at second assignment. Also the first call to check doesn't work, and the type is dependent on overloaded check functions.

What did I expect to happen?

Let's examine the example. The following lines are similar while I expected them to work:

// ...

p1: point;
/* Many statements are here... */
p1 = (0, 0);
/* Many statements are here too... */
p1 = (0, 0); // BUG!

// ...

The first assignment is for object initialization and it works, but the second assignment doesn't work. It leads to surprises at least for novice programmers like me, whereas initialization and assignment are unified with operator= in Cpp2.

Solutions

Three options are available.

1. Make it a syntax error. Do not support it at all.

As simple as that:

p0: point = (0, 0); // OK. It calls the constructor.
p0 = (0, 0); // ERROR!

p1: point;
p1 = (0, 0); // OK. It calls the constructor.
p1 = (0, 0); // ERROR!

2. Support constructor call on assignment.

In assignments the type is completely known (unlike function calls in which positional argument types depend on declared overloaded functions), and (...) for object construction could be supported without any ambiguous.

I have to mention we have two types of constructors: implicit and explicit (the default). The difference between initializations and assignments is that, initializations (first assignments) always works with both implicit and explicit constructors, but assignments (except the first one) won't work with explicit constructors without any extra syntax.

So the previous example for implicit constructor will be like this:

p0: point = (0, 0); // OK. It calls the implicit constructor.
p0 = (0, 0); // OK. It calls the implicit constructor to assign to the variable.

p1: point;
p1 = (0, 0); // OK. It's the first assignment. It calls the implicit constructor.
p1 = (0, 0); // OK. It calls the implicit constructor to assign to the variable.

The above example should work on assignments for implicit constructors, but unnamed objects are required on assignments for explicit constructors. It's like this for an explicit constructor which has only one argument of type int:

number: type = {
    operator=: (out this, n: int) = {}
}

n0: number = 2;
n0 = 2; // ERROR! It doesn't have implicit constructor.
n0 =: number = 2; // OK. It calls explicit constructor.

Now, that example would be like this for explicit constructors:

p0: point = (0, 0); // OK. It calls the explicit constructor.
p0 =: point = (0, 0); // OK. It calls the explicit constructor to assign to the variable.

p1: point;
p1 = (0, 0); // OK. It's the first assignment. It calls the explicit constructor.
p1 =: point = (0, 0); // OK. It calls the explicit constructor to assign to the variable.

The point about this alternative solution is that it's consistent with unnamed objects, at the same time the syntax of initialization and assignment will be distinct for explicit constructors (but they are the same for implicit constructurs).

This syntax complements arrays and dictionaries in Cpp2 as described in this comment. Similarly this feature can be supported for compound assignment operators to directly call a constructor.
In this way, we can easily work with arrays, dictionaries and other objects:

// (key, value) are the arguments for object constructor of `std::pair<std::string, int>`.
c0: my::dictionary<int> = [("a", 1), ("b", 2)];

// Now the dictionary have a different value.
// This is an implicit constructor.
c0 = [("a", 10), ("b", 20)];

// Directly call the constructor and add objects to the dictionary.
c0 += [("c", 30)];

Similar rule applies to -=, *=, /= and other compound assignment operators.

3. Make all assignments to implicitly call constructors

That is the goal of Cpp2 to unify assignments and initializations (e.g. operator=). It doesn't matter if a constructor is implicit or explicit for assignment operators including compound assignment operators (e.g. +=) and return statements:

p0: point = (0, 0); // OK. It calls the explicit/implicit constructor.
p0 = (0, 0); // OK. It calls the explicit/implicit constructor to assign to the variable.

p1: point;
p1 = (0, 0); // OK. It's the first assignment. It calls the explicit/implicit constructor.
p0 = (0, 0); // OK. It calls the explicit/implicit constructor to assign to the variable.

run: () -> point = {
    return (0, 0); // OK. It calls the explicit/implicit constructor and returns.
}

But implicit and explicit constructors have different behaviours in other expressions and operators such as passing arguments in function calls and etc. For example:

number: type = {
    operator=: (out this, n: int) = {}
}

point: type = {
    operator=: (out this, x: int, y: int) = {}
}

a: = (: number = 2) + 2; // ERROR! It would work if it had `implicit` constructor.
a: = (: number = 2) + (: number = 2); // OK.

a: = (: point = (0, 0)) + (0, 0); // ERROR! It would work if it had `implicit` constructor.
a: = (: point = (0, 0)) + (: point = (0, 0)); // OK.

call(2); // ERROR! It would work if it had `implicit` constructor.
call(: number = 2); // OK.

call((0, 0)); // ERROR! It would work if it had `implicit` constructor.
call(: point = (0, 0)); // OK.

Conclusion

I think:

  • Option 1 doesn't solve anything. Simply banning a feature doesn't make sense.
  • Option 2 is good enough.
  • Option 3 fits to the goal of Cpp2 very well. IMO I like this one.

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

No.

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?

Yes. Initialization (first assignments) and assignments (except first assignments) are unified with operator= in Cpp2. It leads to surprises at least for novice programmers if visually that's not true.

Edits

  1. I've replaced option 3.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions