Disambiguating const/non-const overloaded class methods

In C++ one is allowed to create a class containing 2 methods, both with the same name, differing strictly in “constness”

An example of this would be:

class Foo {
  int bar(int a);
  int bar(int a) const;
}

Importing these 2 methods into Swift results in an ambiguity issue since both methods in C++ would map to the same method in Swift.

To disambiguate these 2 methods we need to address 2 main things:

  1. What determines that 2 methods only differ in “constness”
  2. How should we go about differentiating them when importing into Swift

What determines that 2 methods only differ in “constness”

Since we can easily check if a method is marked const or not, this question really comes down to figuring out when we can say 2 methods are overloads of each other.

This issue seems hard as methods can

  • Be templated
  • Have same number of arguments but different types
  • Same number of arguments & types but be in a different order
  • Different return types but same arguments

The simplest approach would be to consider (for the purpose of disambiguating) 2 methods overloads of each other if they are in the same class/struct (record) and have the same name.

One example of a trade-off here would be that the following class:

class Foo {
  int add(int a, int b) const;
  void add(int a);
}

would have its methods imported as:

func add(_ a: Int32, _ b: Int32) -> Int32
func addMutating(_ a: Int32)

since they both share the same name and one is const while the other is not. Even though in reality this methods are not overloads and could both be imported without modification.

On the other hand

class Foo {
  int add(int a, int b) const;
  int add(int a) const;
}

would still get imported as:

func add(_ a: Int32, _ b: Int32) -> Int32
func add(_ a: Int32) -> Int32

since both are marked const we know they are not overloads that differ by “constness”.

We could also do a more complicated approach for determining if 2 methods are overloads by taking arguments and return type into account. Something like Hash(FuncName + Arguments + Return)

This would likely still not be 100% correct at determining 2 methods are strict overloads of each other (templates) but would help in the case shown above.

It seems uncommon that 2 methods in the same class have the same name and differ in “constness” but are not overloads of each other. For that reason it maybe worth just deciding off name alone.

On the other hand, as a user, it may seem weird that two methods which are not overloads of each other result in this modified output (one suffixed with “Mutating”). A more consistent approach maybe to append “Mutating” at the end of every method not marked const . This would be unfortunate though.

How should we go about differentiating them when importing into swift

As shown above one approach would be by modify the name of the mutable version to barMutating .

The mapping would then be:

func bar(_ a: Int32) -> Int32
func barMutating(_ a: Int32) -> Int32

Another potential approach would be to prefix the mutable version with mutating giving:

func bar(_ a: Int32) -> Int32
func mutatingBar(_ a: Int32) -> Int32
3 Likes

Thanks for working on this!

A more consistent approach maybe to append “Mutating” at the end of every method not marked const . This would be unfortunate though.

That would be unfortunate indeed. I think we should not rename methods that don't have a corresponding const method.

I think it's probably okay to consider whether there's a difference in constness between two methods based solely on the method's name. If you try to match a related method to see if it's const or not using the underlying function type, you might run into a certain issues related to matching methods by their parameter signature. You mentioned templates as being a potential problematic case already. Even without them there is a challenge that's caused by default parameter values, as in this case you probably want to rename the second overload to be "mutating" even though their signature differs:

struct X {
int increment(int a) const; 
int increment(int a, int b = 10); 
};

x.increment(10) // the overload depends on the constness of `x`

I think that having a less complex rule here that states that a "mutating" rename is done when there's a const method in the same class with the same name could be easier for our users to grasp. The users will also be able to use an attribute to specify the Swift name of a method if they are not happy with the default rule.

As shown above one approach would be by modify the name of the mutable version to barMutating .

I wonder about readability here. If we consider some uses cases from the C++ standard library where this rule would kick in:

std::map {
  iterator begin();
  const_iterator begin() const;

  T& at( const Key& key );
  const T& at( const Key& key ) const;

  iterator find( const Key& key );
  const_iterator find( const Key& key ) const;
}

Then it feels like there's some potential semantic difference beginMutating / mutatingBegin , atMutating / mutatingAt, and findMutating / mutatingFind when you read this kind of method invocation. Specifically , when I consider beginMutating vs mutatingBegin, I read beginMutating and I think of applying some state that lets me start mutation of the instance of the object that's the receiver of the method call, so to me mutatingBegin seems more clear. I also prefer mutatingAt as well. I don't have a strong opinion about findMutating or mutatingFind, they both seem a little bit unclear to me. For find it might be better to have an explicit rename to something like findMutableIterator.

Overall in my opinion, it seems to me that the default rule of having mutating as a prefix could produce slightly more readable code. I wonder if other people in this community agree or disagree with this opinion.

4 Likes

I should just mention that “mutating” is a bit of a head-scratcher by Swift API naming guidelines. The “ed/ing” rule states that it’s the non-mutating API which gets the “ing” (i.e., “add” is mutating and “adding” is non-mutating), so the word “mutating” is in contradiction with itself when used in this way.

1 Like

That usually is a suffix though to the method call right? Seemingly prepending mutating would not violate that. mutatingAdd.

Also I feel like that has to do with appending ing to the func name itself (add to adding). Adding an entire new work "mutate" may not follow under that same guidance? Not sure what others feel

1 Like

To be slightly pedantic, for C++ types imported as Swift structs, the full declarations would be this, right? (Ignoring the name mangling for the time being.)

mutating func bar(_ a: Int32) -> Int32
func bar(_ a: Int32) -> Int32

We still have the same problem; overloading based on mutating alone isn't allowed in Swift. But would it be feasible to bend those rules for types imported from C++ so that we could have both, and then use the same overload resolution rules in C++? So if a method has mutating and non-mutating overloads, the mutating overload would be invoked when called on a mutable receiver and the non-mutating one would be called on an immutable receiver?

This doesn't address what we would need to do for any C++ types that did get imported as Swift classes (some of these might still exist, from the manifesto?) since mutating wouldn't be applicable there. I don't know how common those are compared to types that would be imported as structs.

But if the common case is that many/most C++ types are imported as structs, then it would be great to have as-natural-as-possible ergonomics for those and save awkward conventions for the special cases, where they matter less (and just let the user annotate the code to improve the name).

6 Likes

The method could be read as “x, mutating, add y” (that is, make a copy of x and mutate that copy by adding y). There exist other ways of spelling this that don’t have this problem—for instance, add versus addConst—and I’m just pointing out the issue so that alternatives are considered.

3 Likes

@xwu good points. addConst or constAdd are options as well!

I think there's a category of these overload pairs that can be mapped into the accessors of properties and subscripts. All the others probably ought to have distinct names.

Can we really translate C++ const to mean non-mutating in Swift? A const member function in C++ can still mutate a struct when that struct has a member designated mutable. Swift has no such loophole...

Sure we can, and it's already being done in the common case. There are conditions where a const method will still be imported as mutating, such as

  • when its containing type has mutable members (see #38618)
  • when it's annotated with __swift_attr__("mutating"), to handle situations such as a const method that mutates this by using const_cast (see #39999)

Even though C++ provides all sorts of loopholes to violate guarantees you've placed in your code, letting those loopholes dictate the ergonomics of the entire interop surface would make it unbearable to use for many common cases, and I'm glad the folks involved in its design/implementation have not been doing that.

2 Likes

We discussed this issue in the Swift and C++ interoperability workgroup, over two meetings (C++ Interop workgroup meeting notes (Feb 23rd 2022) and C++ Interop workgroup meeting notes (Feb 28th 2022)).

The workgroup concluded that a similar ambiguity exists for methods with specific value categories, like lvalue / rvalue methods, and that using a specific naming scheme to disambiguate might not be the best approach in terms of scalability. The recommendation here is to pivot to importing a method overload set using the original method name, and not to rename non- const methods. The right overload for const or non- const , or lvalue/rvalue would then be chosen at the SIL level based on the category of the Swift self value. Users would be able to use an "escape hatch", by changing the value category of the Swift declaration, for example:

let foo = Foo()
foo.bar(2) // calls the const `bar` method.

// escape hatch to call non-const
var mutFoo = foo
mutFoo.bar(2) // calls the non-const `bar` method.
9 Likes