Vittorio Romeo
http://vittorioromeo.info | [email protected]
12 october 2014
When writing both library and user code, in many occasions functions, such as setters, take arguments that will be retained:
class Person
{
private:
std::string name, surname;
public:
void setName(const std::string& mName) { name = mName; }
void setSurname(const std::string& mSurname) { surname = mSurname; }
};
As you can see from the example, the passed mName
and mSurname
arguments will be retained in the Person
instance. From now on, I will refer to this kind of arguments as "sink" arguments.
There is a big issue with sink arguments: they can come from l-values (which need to be copied to be retained), or r-values (which can simply be moved to be retained).
The programmer may be tempted to write something like:
class Person
{
private:
std::string name, surname;
public:
// Pass-by-value + `std::move` idiom
void setName(std::string mName) /* noexcept? */ { name = std::move(mName); }
void setSurname(std::string mSurname) /* noexcept? */ { surname = std::move(mSurname); }
};
Unfortunately, this is not optimal:
- If the passed argument is an l-value, it is copied, then moved. An unnecessary move is performed.
- If the passed argument is an r-value, it is moved. No unnecessary operations are performed.
noexcept
specifier can be problematic in the case that an exception is thrown during the copy.
To avoid unnecessary copies/moves and maintain exception safety, the programmer is forced to either:
-
Write
(2 ^ n)
combinations of functions (wheren
is the number of sink arguments)class Person { private: std::string name, surname; public: // Sink from l-values void setName(const std::string& mName) { name = mName; } void setSurname(const std::string& mSurname) { surname = mSurname; } // Sink from r-values void setName(std::string&& mName) noexcept { name = std::move(mName); } void setSurname(std::string&& mSurname) noexcept { surname = std::move(mSurname); } };
This approach is extremely verbose and can get out of control easily for constructors or functions that take many sink arguments. Huge code repetition (or heavy usage of macros) is necessary to implement this strategy in dealing with sink arguments.
-
Use perfect forwarding
class Person { private: std::string name, surname; public: template<typename T> void setName(std::string&& mName) noexcept(std::is_nothrow_assignable<std::string&, T>::value) { name = std::forward<T>(mName); } template<typename T> void setSurname(std::string&& mSurname) noexcept(std::is_nothrow_assignable<std::string&, T>::value) { surname = std::forward<T>(mSurname); } };
This approach is still extremely verbose, requires the use of templates (forcing the code to be in header files), and prevents easy overloading - SFINAE must be used.
Both of these possible solutions are unoptimal and bothersome for the programmer to deal with.
This proposal is a core language addition proposal. Existing code will not be affected.
I propose the addition of a new contextual keyword, sink
, that will force the compiler to automatically "apply" the (2 ^ n)
overloads solution by translating code written like this:
class Person
{
private:
std::string name, surname;
public:
void setName(sink std::string mName) noexcept(sink) { name = mName; }
void setSurname(sink std::string mSurname) noexcept(sink) { surname = mSurname; }
};
to
class Person
{
private:
std::string name, surname;
public:
void setName(const std::string& mName)
noexcept(std::is_nothrow_assignable<std::string&, const std::string&>::value)
{ name = mName; }
void setSurname(const std::string& mSurname)
noexcept(std::is_nothrow_assignable<std::string&, const std::string&>::value)
{ surname = mSurname; }
void setName(std::string&& mName) noexcept
noexcept(std::is_nothrow_assignable<std::string&, std::string&&>::value)
{ name = std::move(mName); }
void setSurname(std::string&& mSurname)
noexcept(std::is_nothrow_assignable<std::string&, std::string&&>::value)
{ surname = std::move(mSurname); }
};
similarly to what the for(auto x : y)
code expansion does.
The sink
contextual keyword can only be used in two situations:
-
In a function signature declaration/definition, before/after the argument typee
// Allowed void setName(sink std::string mName) { name = mName; } // Not allowed, `sink` can not be applied to const types void setName(sink const std::string mName) { name = mName; } // Not allowed, `sink` can not be applied to reference types void setName(sink std::string& mName) { name = mName; } // Not allowed, `sink` can not be applied to pointer types void setName(sink std::string* mName) { name = *mName; }
-
In a
noexcept(...)
specifier// Allowed, `noexcept`-ness of the function will be deduced on whether or not // the sink argument came from a temporary value or an l-value void setName(sink std::string mName) noexcept(sink) { name = mName; } // Allowed, `sink` can coexist with other conditions void setName(sink std::string mName) noexcept(sink && some_condition(...)) { name = mName; }
The following code
struct Example
{
std::string s1, s2;
Example(sink std::string mS1, sink std::string mS2) noexcept(sink) : s1{mS1}, s2{mS2} { }
};
will be equivalent to
struct Example
{
std::string s1, s2;
Example(std::string&& mS1, std::string&& mS2)
noexcept(std::is_nothrow_assignable<std::string&, std::string&&>::value)
: s1{std::move(mS1)}, s2{std::move(mS2)} { }
Example(const std::string& mS1, std::string&& mS2)
noexcept(std::is_nothrow_assignable<std::string&, std::string&&>::value
&& std::is_nothrow_assignable<std::string&, const std::string&>::value)
: s1{mS1}, s2{std::move(mS2)} { }
Example(std::string&& mS1, const std::string& mS2)
noexcept(std::is_nothrow_assignable<std::string&, std::string&&>::value
&& std::is_nothrow_assignable<std::string&, const std::string&>::value)
: s1{std::move(mS1)}, s2{mS2} { }
Example(const std::string& mS1, const std::string& mS2)
noexcept(std::is_nothrow_assignable<std::string&, const std::string&>::value)
: s1{mS1}, s2{mS2} { }
};