Skip to content

Instantly share code, notes, and snippets.

@vittorioromeo
Created October 12, 2014 13:43
Show Gist options
  • Save vittorioromeo/7a945b7fa0895bb2ea3c to your computer and use it in GitHub Desktop.
Save vittorioromeo/7a945b7fa0895bb2ea3c to your computer and use it in GitHub Desktop.

Vittorio Romeo
http://vittorioromeo.info | [email protected]
12 october 2014

Proposal: language feature to efficiently pass "sink" arguments


1. Motivation

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:

  1. Write (2 ^ n) combinations of functions (where n 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.

  2. 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.


2. Impact on the Standard

This proposal is a core language addition proposal. Existing code will not be affected.


3. Proposed design

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.

3.1 sink keyword

The sink contextual keyword can only be used in two situations:

  1. 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; }
    
  2. 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; }
    

4. Example expansions

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} { } 
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment