Skip to content

CPP-5096 S1236 Explain covered cases and exception #3925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rules/S1236/cfamily/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Assignment operators should return non-\"const\" references",
"title": "Assignment operators should return non-\"const\" reference to assigned object",
"type": "CODE_SMELL",
"code": {
"impacts": {
Expand Down
195 changes: 179 additions & 16 deletions rules/S1236/cfamily/rule.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,199 @@

Copy assignment operators and move assignment operators can return anything, including ``++void++``.

However, if you decide to declare them yourself (don't forget the "Rule-of-Zero", S4963),
it is a recommended practice to return a non-const reference to the assigned object (left-operand).
It allows the developer to chain the assignment operations, increasing consistency with what other types do and, in some cases, enabling the writing of concise code.

However, if you decide to declare them yourself (don't forget the "Rule-of-Zero", S4963), it is a recommended practice to return a non-const reference to the left-operand. It allows the developer to chain the assignment operations, increasing consistency with what other types do, and in some cases enabling writing concise code.
This rule will raise for assignment operators that deviate from the above expectation.

=== Using unconventional return type

=== Noncompliant code example
This rule will raise an issue if the return type of the copy or move assignment operator,
is different from mutable reference to the class type.

[source,cpp]
==== Noncompliant code example

[source,cpp,diff-id=1,diff-type=noncompliant]
----
class Clazz {
public:
const Clazz& operator=(const Clazz& other); // Noncompliant, returns const reference
Clazz operator=(Clazz&& other) noexcept; // Noncompliant, returns by value
};
----

==== Compliant solution

[source,cpp,diff-id=1,diff-type=compliant]
----
class Clazz {
public:
Clazz& operator=(const Clazz& other); // Compliant
Clazz& operator=(Clazz&& other) noexcept; // Compliant
};
----

=== Returning object different that ``++*this++``

An assignment operator should return a reference to the assigned object.
Conventionally, such return is expressed as ``++return *this++``, and the rule will mark any return statement as deviating from this convention.

==== Noncompliant code example

[source,cpp,diff-id=2,diff-type=noncompliant]
----
class Clazz {
public:
Clazz& set(Clazz& other);
Clazz& operator=(Clazz const& other) {
return set(other); // Noncompliant, depends on return of `set` member function
}

Clazz&& operator=(Clazz&& other) noexcept {
return other; // Noncompliant, also return type is non-compliant
}
};
----

==== Compliant solution

[source,cpp,diff-id=2,diff-type=compliant]
----
class Clazz {
public:
Clazz& set(Clazz& other);
Clazz& operator=(Clazz const& other) {
set(other);
return *this; // Compliant
}

Clazz& operator=(Clazz&& other) noexcept {
return *this; // Compliant
}
};
----

In {cpp}23, if the assignment operator is declared using an explicit object argument,
the rule will mark any return statement that does not return the object parameter directly.

==== Noncompliant code example

[source,cpp,diff-id=3,diff-type=noncompliant]
----
class Clazz {
public:
Clazz& set(Clazz& other);
Clazz& operator=(this Clazz& self, Clazz const& other) {
return self.set(other); // Noncompliant, depends on the return of `set` member function
}
};
----

==== Compliant solution

[source,cpp,diff-id=3,diff-type=compliant]
----
class Clazz {
public:
Clazz& set(Clazz& other);
Clazz& operator=(this Clazz& self, Clazz const& other) {
self.set(other);
return self; // Compliant
}
};
----

=== Declaring assignment operation as non-mutating

The assignment operation is designed to change the value of the target object,
to the same one as the source.
Such operations are mutating and thus should not be declared with a `const` qualifier.

==== Noncompliant code example

[source,cpp,diff-id=4,diff-type=noncompliant]
----
class Clazz {
public:
Clazz& operator=(Clazz const& other) const { // Noncompliant, also least to noncompliant return statement
return const_cast<Clazz&>(*this);
}
Class& operator=(Clazz&& other) const; // Noncompliant
};
----

==== Compliant solution

[source,cpp,diff-id=4,diff-type=compliant]
----
class Clazz {
public:
Clazz& operator=(Clazz const& other) const { // Compliant
return *this;
}
Clazz& operator=(Clazz&& other) const; // Compliant
};
----

When declaring assignment operator with {cpp}23 explicit object argument,
an assignment operator can be declared as non-mutation by either passing object argument:

* by const reference - this is equivalent to declaring the implicit object parameter method as `const`,
as described above;
* by value - in this case a temporary object will be created, and modified by the assignment operator,
instead of the left-hand side of the assignment operator

==== Noncompliant code example

[source,cpp,diff-id=5,diff-type=noncompliant]
----
class Clazz {
int val;
public:
Clazz& operator=(this Clazz const& self, Clazz const& other) const { // Noncompliant, also leads to non-compliant return
return const_cast<Clazz&>(self);
}
void operator=(this Clazz self, Clazz&& other) { // Noncompliant
self.val = other.val; // Modifies temporary object
}
};
----

==== Compliant solution

[source,cpp,diff-id=5,diff-type=compliant]
----
class A {
class Clazz {
int val;
public:
~A() = default;
A(A const &) = default;
A(A&&) = default;
const A& operator=(const A& other) ; // Noncompliant
A operator=(A&& other) noexcept; // Noncompliant
Clazz& operator=(this Clazz& self, Clazz const& other) { // Compliant
self.val = other.val;
return self;
}
Clazz& operator=(this Clazz& self, Clazz&& other) { // Compliant
self.val = other.val; // Modifies referenced object
return self;
}
};
----

=== Exceptions

=== Compliant solution
This rule will not raise the issue for the return type of the assignment operator which is declared as `void.`
Such syntax is commonly used in situations where changing the assignment operator is not desired.
The issue will still be raised if such an assignment operator is declared as non-mutating.

[source,cpp]
----
class A {
class Clazz {
int val;
public:
~A() = default;
A(A const &) = default;
A(A&&) = default;
A& operator=(const A& other);
A& operator=(A&& other) noexcept;
void operator=(Clazz const& other) { // Compliant
self.val = other.val;
return self;
}
void operator=(Clazz&& other) const; // Noncomplaint, declared as const
};
----

Expand Down