std::sink<T> to efficiently pass `sink` arguments

742 views
Skip to first unread message

Vittorio Romeo

unread,
Sep 10, 2013, 11:29:18 AM9/10/13
to std-pr...@isocpp.org

Inspired by my own StackOverflow questionShould I always move on sink constructor or setter arguments?

Problem: passing sink arguments efficiently either requires performance trade-offs or extensive code duplication

Current possible approaches

Approach 1

Pass by value and std::move

struct Example {
    std::string s1, s2;
    Example(std::string mS1, std::string mS2) : s1{std::move(mS1)}, s2{std::move(mS2)} { } 
};
  • Passed argument is a temporary: it is moved without copies. Result: 1 move.
    Passed argument is an lvalue: it is copied, then moved. Result: 1 copy + 1 move.
  • Pros: no code duplication.
    Cons: unnecessary move when the passed argument is an lvalue.

Approach 2

Multiple constructors: pass by const& and &&

struct Example {
    std::string s1, s2;
    Example(std::string&& mS1,      std::string&& mS2)      : s1{std::move(mS1)}, s2{std::move(mS2)} { } 
    Example(const std::string& mS1, std::string&& mS2)      : s1{mS1}, s2{std::move(mS2)} { } 
    Example(std::string&& mS1,      const std::string& mS2) : s1{std::move(mS1)}, s2{mS2} { } 
    Example(const std::string& mS1, const std::string& mS2) : s1{mS1}, s2{mS2} { } 
};
  • Passed argument is a temporary: it is moved without copies. Result: 1 move.
    Passed argument is an lvalue: it is just copied. Result: 1 copy.
  • Prosmost efficient behavior and code generation.
    Cons: requires n^2 constructors/functions written by the developer!


Proposal: passing with std::sink<T>by value - behaves identically to Approach 2, avoids code duplication

struct Example {
    std::string s1, s2;
    Example(std::sink<std::string> mS1, std::sink<std::string> mS2) : s1{mS1}, s2{mS2} { } 
};

The above code behaves as if the user had written n^2 constructors similarly to Approach 2.

Thoughts?

Martinho Fernandes

unread,
Sep 10, 2013, 11:31:06 AM9/10/13
to std-pr...@isocpp.org
Erm, no, not without considering approach 3, perfect forwarding, first.

Vittorio Romeo

unread,
Sep 10, 2013, 11:32:04 AM9/10/13
to std-pr...@isocpp.org
> Date: Tue, 10 Sep 2013 17:31:06 +0200
> Subject: Re: [std-proposals] std::sink<T> to efficiently pass `sink` arguments
> From: martinho....@gmail.com
> To: std-pr...@isocpp.org

>
>
> Erm, no, not without considering approach 3, perfect forwarding, first.

How does perfect forwarding help here? Or is it an issue?

Martinho Fernandes

unread,
Sep 10, 2013, 11:36:42 AM9/10/13
to std-pr...@isocpp.org
Perfect forwarding was added to the language in order to solve the N^2
overloads problem. Of course it can help here.

Mit freundlichen Grüßen,

Martinho

Jeffrey Yasskin

unread,
Sep 10, 2013, 11:43:20 AM9/10/13
to std-pr...@isocpp.org
Before saying "of course", please write the code.

Nevin Liber

unread,
Sep 10, 2013, 11:44:24 AM9/10/13
to std-pr...@isocpp.org
On 10 September 2013 10:36, Martinho Fernandes <martinho....@gmail.com> wrote:

> How does perfect forwarding help here? Or is it an issue?

Perfect forwarding was added to the language in order to solve the N^2
overloads problem. Of course it can help here.

Yeah, but it is a bit painful to use, as you end up needing to make all the parameters templates and then use SFINAE or Concepts Lite to limit the damage.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Vittorio Romeo

unread,
Sep 10, 2013, 11:45:13 AM9/10/13
to std-pr...@isocpp.org
> From: jyas...@google.com
> Date: Tue, 10 Sep 2013 08:43:20 -0700

> Subject: Re: [std-proposals] std::sink<T> to efficiently pass `sink` arguments


> Before saying "of course", please write the code.
>

Indeed. Also, wouldn't it require to use templates with the so-called universal references?

Vittorio Romeo

unread,
Sep 10, 2013, 11:47:26 AM9/10/13
to std-pr...@isocpp.org
On Tuesday, 10 September 2013 17:44:24 UTC+2, Nevin ":-)" Liber wrote:
Yeah, but it is a bit painful to use, as you end up needing to make all the parameters templates and then use SFINAE or Concepts Lite to limit the damage.

If that's the case, it's even worse than the n^2 constructor problem.
Also, I think specifying that the argument is gonna be sinked right in the signature (in my std::sink<T> proposal) clearly expresses the intent, more so than forwarding or pass-by-value + move idiom.

rhalb...@gmail.com

unread,
Sep 10, 2013, 12:04:01 PM9/10/13
to std-pr...@isocpp.org
On Tuesday, September 10, 2013 5:29:18 PM UTC+2, Vittorio Romeo wrote:
  • Cons: requires n^2 constructors/functions written by the developer!


Actually: 2^N overloads (a factor of 2 for each of the N arguments).

Billy O'Neal

unread,
Sep 10, 2013, 12:16:49 PM9/10/13
to std-proposals
Sounds interesting. How would you implement this though?

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


--
 
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Vittorio Romeo

unread,
Sep 10, 2013, 12:21:41 PM9/10/13
to std-pr...@isocpp.org
While looking for a possible solution I found this:

http://cpptruths.blogspot.it/2012/10/c11-idioms-2012-silicon-valley-code-camp.html

All the slides are related to the problem, but a possible (albeit, in my opinion, not perfect) implementation can be found on slide 29.

Billy O'Neal

unread,
Sep 10, 2013, 1:30:37 PM9/10/13
to std-proposals
I'm not sure that implementation works. Presumably you'd want to call the move member, but that member is not eligible for RVO (does not return the same variable on all code paths, and does not return rvalues on all code paths). So you end up just paying the move when moving the return value of in<T>::move() into the target.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


--

Vittorio Romeo

unread,
Sep 10, 2013, 1:37:31 PM9/10/13
to std-pr...@isocpp.org
You're right, I think. Maybe an implementation would require some changes to the core language: my suggestion is something like std::initializer_list, it looks like a library-only class but actually is bound to new language ideas


From: billy...@gmail.com
Date: Tue, 10 Sep 2013 10:30:37 -0700

Subject: Re: [std-proposals] std::sink<T> to efficiently pass `sink` arguments
To: std-pr...@isocpp.org

Vittorio Romeo

unread,
Sep 11, 2013, 9:33:13 AM9/11/13
to std-pr...@isocpp.org
Sorry for the duplicate thread. As Jonathan Wakely suggested, let's just use this one to avoid unnecessary duplication.

In the other thread, Jared Grubb mentioned:
At GoingNative last week, someone mentioned that "move elision" will (probably?) be added to the standard. That means the compiler is allowed to remove moves in the same way compilers are allowed to remove copies. 
I think that would apply to your example, and so the results would just be "1 move" and "1 copy", respectively. But, ask a guru, since I'm not 100% certain.

Jonathan Wakely replied to him:
That's already allowed in C++11.

 This sounds a good solution. However, searching online for "move elision" returns 0 relevant results.
Can someone confirm this?

Daniel Krügler

unread,
Sep 11, 2013, 9:39:50 AM9/11/13
to std-pr...@isocpp.org
2013/9/11 Vittorio Romeo <vittorio....@gmail.com>
The standard does not have a separate name for it, so there exists not the term "move elision".  The definition of copy elision starts as follows:

"This elision of copy/move operations, called copy elision, is permitted in the following circumstances [..]"

- Daniel

Billy O'Neal

unread,
Sep 11, 2013, 12:47:03 PM9/11/13
to std-proposals
The standard doesn't allow move elision from xvalues (as described at G::N). That is, given
 
expensive_t go()
{
    expensive_t result;
    return move(result);
}
Implementations aren't allowed to use RVO/NRVO here for the result of std::move(), even though this would normally be an NRVO candidate.
 

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


--

Richard Smith

unread,
Sep 11, 2013, 1:49:46 PM9/11/13
to std-pr...@isocpp.org
On Wed, Sep 11, 2013 at 9:47 AM, Billy O'Neal <billy...@gmail.com> wrote:
The standard doesn't allow move elision from xvalues (as described at G::N). That is, given
 
expensive_t go()
{
    expensive_t result;
    return move(result);
}
Implementations aren't allowed to use RVO/NRVO here for the result of std::move(), even though this would normally be an NRVO candidate.

See NB comments US12 and US13.

Daniel Krügler

unread,
Sep 11, 2013, 2:07:25 PM9/11/13
to std-pr...@isocpp.org

2013/9/11 Billy O'Neal <billy...@gmail.com>

The standard doesn't allow move elision from xvalues (as described at G::N). That is, given
 
expensive_t go()
{
    expensive_t result;
    return move(result);
}
Implementations aren't allowed to use RVO/NRVO here for the result of std::move(), even though this would normally be an NRVO candidate.

That's correct. There exists several proposals to fix that, among them one that gives std::move() a special meaning for the core language.

That is one of the to clearly specify what someone means. The term "move elision" alone is not specific enough.

- Daniel

Vittorio Romeo

unread,
Sep 12, 2013, 9:01:12 AM9/12/13
to std-pr...@isocpp.org
I'm confused: is the compiler able to elide or not in this situation?

Daniel Krügler

unread,
Sep 12, 2013, 9:11:11 AM9/12/13
to std-pr...@isocpp.org
2013/9/12 Vittorio Romeo <vittorio....@gmail.com>

I'm confused: is the compiler able to elide or not in this situation?

It is currently not in this specific situation, but that doesn't mean that there is no move-elision going on. For example a minor simplification of above example *does* enable move-elision:

expensive_t go()
{
    expensive_t result;
    return result;
}

- Daniel

Vittorio Romeo

unread,
Sep 12, 2013, 9:28:50 AM9/12/13
to std-pr...@isocpp.org
That's `traditional` move/copy elision in my book - the typical elision done on return from functions.
I'd say it's not a simplification of my examples but a completely different thing.
So I guess not having move-elision on `sink` arguments can still be considered a `defect`.



Date: Thu, 12 Sep 2013 15:11:11 +0200
Subject: Re: [std-proposals] Re: std::sink<T> to efficiently pass `sink` arguments
From: daniel....@gmail.com
To: std-pr...@isocpp.org

Daniel Krügler

unread,
Sep 12, 2013, 10:14:57 AM9/12/13
to std-pr...@isocpp.org
2013/9/12 Vittorio Romeo <vittori...@outlook.com>

That's `traditional` move/copy elision in my book - the typical elision done on return from functions.

Exactly - which is also elision on move operations.
 
I'd say it's not a simplification of my examples but a completely different thing.
So I guess not having move-elision on `sink` arguments can still be considered a `defect`.

Please don't use the term move-elision, this is IMO totally confusing.

- Daniel
 

Jonathan Wakely

unread,
Sep 12, 2013, 11:31:19 AM9/12/13
to std-pr...@isocpp.org
On Thursday, September 12, 2013 2:28:50 PM UTC+1, Vittorio Romeo wrote:
That's `traditional` move/copy elision in my book - the typical elision done on return from functions.

Well yes, when I said move elision is allowed in C++11 I was responding to "That means the compiler is allowed to remove moves in the same way compilers are allowed to remove copies." (emphasis mine.)

And it's clearly move elision (not copy elision) if it happens for a type that is not copyable:

struct MoveOnly {
    MoveOnly() = default;
    MoveOnly(MoveOnly&&) { throw 1; }
};

MoveOnly f()
{
    MoveOnly mo;
    return mo;
}

int main()
{
    auto mo = f();
}

With my compilers this program exits normally.

Sumant Tambe

unread,
Sep 14, 2013, 2:30:13 PM9/14/13
to std-pr...@isocpp.org
Hi, 

I'm still catching up with the discussion on this thread. For now, I just want to point to the original blog post that discussed the "sink helper".
The original code is more complex than what I have in the slides: http://codesynthesis.com/~boris/data/argument-passing/in.tar.gz 

Thanks,
Sumant

Billy O'Neal

unread,
Sep 14, 2013, 6:27:07 PM9/14/13
to std-proposals
It has the same problem I described though:
 
T move () const {return lv_ ? *lv_ : std::move (*rv_);}
This is functionally equivalent to pass-by-value. The rule for RVO is that all return paths must be rvalues (not true, lv_ is an lvalue), and for NRVO that all returns are a single local variable (neither conditional is a local variable, and they are different). Therefore, you get a copy of T, either copy constructed or move constructed depending on lv_. That value is move constructed (which may be a copy construction if T has no move constructor) into whatever code called sink::move().
 
As I said, I really would like to see something like this happen, but this implementation is functionally equivalent to the pass-by-value solution. I'm not sure if an implementation that actually works here is possible in the current language.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Sumant Tambe

unread,
Sep 14, 2013, 10:59:27 PM9/14/13
to std-pr...@isocpp.org
The goal is not to do better than pass-by-value-and-move approach. But to allow run-time query on argument lvalue/ rvalue-ness without forcing the use of universal references and exponential explosion argument types when rvalue refs are used. I'm ready to accept a couple of extra moves in return of a single expressive function declaration (and implementation) which modern editors can help autocomplete. in<T> is not meant for constructor arguments because pass-by-value-and-move approach works better over there.

If the idea has any merit, I would prefer "in" over "sink" because arguments dont necessarily sink (like in unique-ptr).

Thanks,
Sumant

Billy O'Neal

unread,
Sep 15, 2013, 12:02:34 AM9/15/13
to std-proposals
How does that cause exponential explosion? Pass by value every time, and you have no overloads.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Sumant Tambe

unread,
Sep 15, 2013, 12:43:26 AM9/15/13
to std-pr...@isocpp.org
I mean to say there are 3 options and in<T> might be the 4th.
1. Using universal refs (forces template)
2. Using rvalue refs and const lvalue refs conbination (causes exponential number of function versions)
3. Pass by value (good only with sink args)
4. Pass as std::in<T>

Billy O'Neal

unread,
Sep 15, 2013, 12:53:51 AM9/15/13
to std-proposals
How is 3 different than 4? They both result in a value copy in all cases.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Sumant Tambe

unread,
Sep 15, 2013, 2:21:46 AM9/15/13
to std-pr...@isocpp.org
Billy,

Please consider the following matrix class and the test code after that:

struct matrix {
  matrix () {}
  matrix (matrix const&) {cout << "copy-ctor, ";}
  matrix (matrix&&) {cout << "move-ctor, ";}
  matrix & operator = (matrix const &) { cout << "copy-assign, "; return *this; }
  matrix & operator = (matrix &&) { cout << "move-assign, "; return *this; }

  matrix& operator+= (const matrix &m) {
    return *this;
  }
  std::vector<int> data;
};

inline matrix operator+ (in<matrix> x, in<matrix> y) {
  return std::move (
    x.rvalue () ? x.rget () += y :
    y.rvalue () ? y.rget () += x :
    matrix (x) += y);
}

    matrix s1;
    matrix s2;
    matrix m1 (matrix() + s1);        cout << endl; (1)
    matrix m2 (matrix() + matrix());  cout << endl; (2)
    matrix m3 (s1 + s2);              cout << endl; (3) 
    matrix m4 (s1 + matrix());        cout << endl; (4)

The question here is how to implement the binary operator +(). Pass-by-value for operator +() ends up making two copies of the matrix every time. in<matrix> version makes a copy only when necessary i.e., in case #3--without directly using T&&, matrix&&, and const matrix &. Note that this just an example and in<T> is usable beyond operator+().

Thanks,
Sumant

Sumant Tambe

unread,
Sep 15, 2013, 2:23:23 AM9/15/13
to std-pr...@isocpp.org
Correction: Pass-by-value approach makes two copies when both parameters are lvalues. Not "everytime". 

Billy O'Neal

unread,
Sep 15, 2013, 3:17:16 AM9/15/13
to std-proposals
Ok, that's interesting.
 
Let's consider 3 possible implementations of operator+. First, the C++03 way:
 
// Example 1
inline matrix operator+ (matrix const& x, matrix const& y) {
  matrix result(x);
  result += y;
  return result;
}
 
This always makes exactly one copy, the construction of result. NRVO removes the copy construction of the return value.
 
An alternate formulation in the land of move-semantics which is still compliant would be this one:
 
// Example 2
inline matrix operator+ (matrix result, matrix const& y) {
  result += y;
  return result;
}
 
which allows replacing the single copy with a move, meaning this always takes a copy or a move, depending on the first argument to operator+. If the left side is an lvalue, then you get a copy; if it is an rvalue, you get a move.
 
Now let's look at your sink solution:
 
// Example 3
inline matrix operator+ (in<matrix> x, in<matrix> y) {
  return std::move (
    x.rvalue () ? x.rget () += y :
    y.rvalue () ? y.rget () += x :
    matrix (x) += y);
}
 
This solution does allow choosing of which side to "steal" from at run time. Let's go through each combination of arguments:
1. Two r value inputs. This causes one move construction in order to construct the return value -- no better than Example 2 above. (The result of a call to std::move() this way is not eligible for RVO/NRVO) It also adds 2 branches not present in example 2, and adds the cost of passing 2 bool sized values as arguments (or more, depending on alignment requirements or whatever).
2. Two l value inputs. This causes one copy construction, (temporary matrix(x)), plus a move to construct the return value of operator+. One extra move not present in example 1 or 2, not counting additional overhead from passing the bools.
3. lvalue on the left, rvalue on the right. In this case, you get one move construction for the return value of operator+. This is an improvement, because example 2 above can't detect that the argument on the right is an rvalue.
4. rvalue on the left, lvalue on the right. In this case, you get one move construction again, but example 2 above already had this property. You lose the same overhead of the branches and bools, of course.
 
So, the same in 2 cases, slightly better in 1 case, slightly worse in 1 case. Don't think this is really worth it. My advice would be to just write example 2 above, and if you profile and find this to be on the hot path and have cases where rvalue references are on the right, then use perfect forwarding or overloads.
 
I worry that because we have move semantics now, people are going to start getting hammer-nail syndrome over them. Yes, they do help perf in some cases, but the complexity they add probably isn't all that important in most places. Listen to your profiler, and add that complexity where it is warranted.
 

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Sumant Tambe

unread,
Sep 15, 2013, 7:48:10 PM9/15/13
to std-pr...@isocpp.org
Thanks for the detailed analysis. I agree that in<T> is slightly better in some cases and slightly inferior in others. I ran a benchmark and saw about 1-2% improvement in performance when using in<T> as opposed to the example #2 you proposed. Each run of the benchmark executed all 4 versions (lvalue-lvalue, lvalue-rvalue, rvalue-lvalue, rvalue-rvalue) of binary operator+ with equal probability (rand() % 4). 

1 to 2 % performance gain may or may not be "worth" it. People should use their judgement. However, story does not end here.

Binary operator +() provides only one opportunity for in<T> to save a copy. I.e.,  when the right hand side object is an rvalue. The in<T> idiom shines the most when there are more opportunities to detect rvalues and to exploit them at run-time. So here is a small function to add scalars to a list of matrices. 

std::vector<matrix>
add_scalar(std::initializer_list<in<matrix>> matrices,
           std::initializer_list<int> scalars)
{
  if(matrices.size() != scalars.size())
    throw 0;

  std::vector<matrix> result;
  result.reserve(matrices.size());
  
  auto si = begin(scalars);
  for(const in<matrix> & m : matrices)
  {
     if(m.rvalue()) {
        cout << "rvalue matrix\n";
        result.push_back(std::move(m.rget() += *si));
     }
     else {
        cout << "lvalue matrix\n";
        result.push_back(std::move(matrix(m) += *si));
     }
  }

  return result;
}

And I call it like this:

matrix s1, s2, s3, s4;
add_scalar({ s1, s1+s2, s1+s3, s1+s4 }, { 3, 5, 7, 9});

I tested this code on g++ 4.9 and clang 3.3.

In each use, this function gives finite but unknown number of opportunities to exploit move-semantics. I'm not sure if such a function can be easily implemented using universal references. Variadic templates come to mind but two lists of parameters probably requires separate grouping. I guess it could be done using std::tuple and two variadic parameter packs of universal references. But std::make_tuple is not as pretty as std::initializer_list.

Surprisingly, in<T> works nicely with initializer_list<T>. Initilizer_list does not appear to retain rvalue/lvalue-ness of the original parameter because initializer_list iterator is a pointer to consts. In<T>, however, allows the rvalue/lvalue knowledge of the original parameter pass through the initializer_list--here is the best part--without subverting const-correctness. All member functions in in<T> are const.  

Thoughts?

Sumant

Bengt Gustafsson

unread,
Sep 16, 2013, 6:11:41 AM9/16/13
to std-pr...@isocpp.org
My take on this would be that what we are looking for is a new type of template parameter which only allows the "reference level" to vary, but not the actual parameter base type, i.e. a universal reference to a specific type. Using such a mechanism you only need to write one function, and the compiler generates the optimal code for each call site depending on its combination of rvalues and lvalues. I don't have a good idea about the syntax, so in the example below I use ++ as a place holder.

matrix operator+(matrix++ x, matrix++ b)
{
    matrix ret(x);
    ret += y;
    return ret;
}

I don't think this is a good syntax, as it is a template without the leading template<> introducer, but maybe it could be food for thought. 

The in<> approach has the definite drawback that there are tests being done at runtime in a function implementation which is intended to have optimum speed. That does not ring true to me. The perfect forwarding approach is very wordy considering that you'd want to enable_if out all other base types, especially for a free function like operator+.

An addition to the constraints idea where a class name used as a constraints name inside the template introducer restricts arguments to that class would have been nice, but clashes with non-typename template parameters of course:

template<matrix&& T1, matrix&& T2> matrix operator+(T1 x, T2 y)
{
   ...
}

Also still pretty wordy. Creating a constexpr function Matrix that restricts the type to matrix would be possible with the constraints idea but I don't know if constraints will allow you to add a && after Matrix like I did above.


Vittorio Romeo

unread,
Sep 16, 2013, 6:20:37 AM9/16/13
to std-pr...@isocpp.org
Indeed, I think this is not a problem that can be solved with the current language features. But it is still a problem that has to be solved.
In my opinion, the best course of action is having some syntax, that, when applied to arguments, makes the compiler generate the 2^n combinations of const& and &&.

Instead of introducing new keywords/symbols, why not use std::sink as a special identifier, similarly to std::initializer_list?

void singleSinkFunc(std::sink<ExpensiveType> a) { doSomething(a); }
// v--- TRANSLATES TO ---v

void singleSinkFunc(ExpensiveType&& a)      { doSomething(std::move(a)); }
void singleSinkFunc(const ExpensiveType& a) { doSomething(a); }

void doubleSinkFunc(std::sink<ExpensiveType> a, std::sink<ExpensiveType> b) { doSomething(a, b); }
// v--- TRANSLATES TO ---v

void doubleSinkFunc(ExpensiveType&& a, ExpensiveType&& b)            { doSomething(std::move(a), std::move(b)); }
void doubleSinkFunc(const ExpensiveType& a, ExpensiveType&& b)       { doSomething(a, std::move(b)); }
void doubleSinkFunc(ExpensiveType&& a,  const ExpensiveType& b)      { doSomething(std::move(a), b); }
void doubleSinkFunc(const ExpensiveType& a,  const ExpensiveType& b) { doSomething(a, b); }



Alternative ideas for syntax:

void sinkFunc(std::in<ExpensiveType> a);
void sinkFunc(>ExpensiveType< a);
void sinkFunc(ExpensiveType<< a);
void sinkFunc(ExpensiveType sink a);
void sinkFunc(sink ExpensiveType a);
void sinkFunc(ExpensiveType in a);
void sinkFunc(in ExpensiveType a);




Alex B

unread,
Sep 16, 2013, 11:07:31 AM9/16/13
to std-pr...@isocpp.org
How do you get a function pointer to one of the four specialization of doubleSinkFunc?
 
Idea of a terse notation sounds nice (sink, in, ...), but if it is a template, there must at least be a way to declare it using the "template" keyword, so that we could mark it explicitly that it needs to be parameterized. And then we could get a pointer to the function taking "const &" by specifying the parameter with a syntax like "doubleSinkFunc<const &>". After all, it is a template, so we should be able to mark it and deal with it explicitly.

Billy O'Neal

unread,
Sep 16, 2013, 12:15:17 PM9/16/13
to std-proposals
Can you post the benchmark code? Not that I don't trust you, but 1-2% is well within the margin of error in most cases. In particular, any time you involve IO (e.g. std::cout) that's going to play havoc with benchmarks.
 
Regarding your code example, that API is poor for 2 reasons:
1. doing in a way that is exception safe is impossible. The way to make it exception safe is just to make copies.
2. that interface forces creation of a new vector on every call.
3. you have way too many unnecessary moves in that example -- use emplace where possible. If T was something like std::array, move is not cheaper than copy.
 
You can fix (1) using something like:
// Note: I'm no initializer_list expert -- you may need to
// have some kind of wrapper type if it isn't legal to take
// a const&.
std::vector<matrix>
add_scalar(std::initializer_list<matrix const&> matrices,
           std::initializer_list<int> scalars)
{
  if(matrices.size() != scalars.size()) {
    assert(false && "Matrix scalar size mismatch!");
    std::terminate(); // Don't throw for things that should terminate!
  }
 
  // Make exception safe copy:
  std::vector<matrix> result(begin(matricies), end(matricies));
  auto si = begin(scalars); // Nothrow
  for(matrix& m : result) // Norhrow
  {
     m += *si++; // Nothrow
  }
 
  return result;
}
 
Most importantly though, this is a niche case, not a common case. The original proposal was trying to support the common case of having a constructor with a sink argument. That is a compelling reason to add things to the standard -- I'm not sure this is, even in the couple of places where it may make sense..

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Alex B

unread,
Sep 16, 2013, 1:49:04 PM9/16/13
to std-pr...@isocpp.org
Thinking about it, maybe concepts will be able to (quite elegantly) solve this problem. Consider a Sink concept (rename it to whatever you like/appropriate).


template <class T, class U>
concept bool Sink()
{
  return is_base_of<T, decay_t<U>>::value;
}

template <class T>
constexpr bool is_mutable_rvalue = !is_const<T>::value && (is_rvalue_reference<T>::value || !is_reference<T>::value);

template <class T>
T& make_mutable(const T& t)
{
   return const_cast<T&>(t);
}


template <Sink<matrix> A, Sink<matrix> B>
matrix operator+(A a, B b)
{
   return move(
      is_mutable_rvalue<A> ? make_mutable(a) += b :
      is_mutable_rvalue<B> ? make_mutable(b) += a :
      matrix(a) += b);
}


--
 
---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/flqtAYA-yMI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

Billy O'Neal

unread,
Sep 16, 2013, 2:04:09 PM9/16/13
to std-proposals
You never want "return move(x)" -- it disables RVO.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


--
 
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.

Xeo

unread,
Sep 16, 2013, 3:14:33 PM9/16/13
to std-pr...@isocpp.org
Not if Richard's NB comment 12 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3733.pdf, search for "US 12") is properly resolved - which allows copy / move elision from xvalues.

Sumant Tambe

unread,
Sep 16, 2013, 3:59:37 PM9/16/13
to std-pr...@isocpp.org
@1 Striving for strong exception safety is a noble goal in general but in the example I showed it seems unnecessary and imposes overhead. What user-visible state are you trying to keep safe from exceptions? The initiaizer_list does not live too long before and after the function it is invoked for. The initializer_list could contain lvalues and those should be protected from exceptions. I do that by making a copy only if there is an lvalue. If an element is rvalue then by definition no one else has seen it and therefore I move from it. Now any exception down the line will free the result vector invoking destructors on the copies of lvalues and the moved from rvalues. The lvalues that the iteration never reached to will remain unchanged. Everything is still exception safe without the overhead of copying all the initializer_list elements upfront and thereby loosing the opportunities to move.
@2 That's right, I want to return a new vector every time.
@3 The moves are necessary in the code I've shown. operator += returns matrix & and must be converted to matrix &&. emplace_back won't do any magic in this case, IMO.

Finally, on the main subject: I'm not sure since when std::initializer_list has become a niche. Does the standard say that? It is less common than writing a constructor, I agree but more likely than a overleaded binary operator.

And here is the benchmark routine. For the record, I don't benchmark with I/O enabled.
void invoke_binary(int iter)
{
  matrix s1, s2;
  for(int i=0;i < iter; ++i)
  {
    int branch = rand() % 4;
    switch(branch)
    {
      case 0: {
        matrix m1 (matrix() + s1);
        break;
      }
      case 1: {
        matrix m2 (matrix() + matrix());
        break;
      }
      case 2: {
        matrix m3 (s1 + s2);
        break;
      }
      case 3: {
        matrix m4 (s1 + matrix());
        break;
      }
    }    
  }
}

Billy O'Neal

unread,
Sep 16, 2013, 4:24:00 PM9/16/13
to std-proposals
1. Then I guess we just fundamentally disagree. Just because something is an rvalue at one point does not mean that it will stay that way if an exception is thrown. For instance, if the matrices come out of a vector with move() applied to the vector's elements. But if an exception is thrown, the caller probably doesn't want the vector to have been modified.
2. Ok, well that sucks for performance. Making the micro-optimization of being able to move in some cases is probably completely overshadowed by having to create new vectors every time for that particular call.
3. No, the moves are not necessary. Emplace them into the vector, and then apply += in their new positions. (As I demonstrated in my example)
4. That benchmark is flawed. On different runs you're executing completely different operations. Not to mention I would be hugely surprised if any compiler didn't just inline the whole thing and defeat the benchmark.

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Sumant Tambe

unread,
Sep 16, 2013, 5:27:06 PM9/16/13
to std-pr...@isocpp.org
The point of the add_scalar example is, well, an example. Something to chew on while I'm trying to get the value of in<T> across as I see it. Possible overshadowing of move optimizations within that function are tangential to the main discussion. And so it the exception safety of the implementation. And so is the fact that I'm creating a vector each time.

in<T> gives a way to separate lvalues from rvalues at run-time and I'm trying to solicit feedback on a pure library-based approach. Language extensions tend to be substantially more complex, error-prone, harder to convince (due to almost limitless freedom) and hard to test unless you know a rockstar compiler dev. I'm trying hard to pickup useful nuggets here.

1. in<T> may interact with exception safety and may possibly improve/degrade it. But that is not an inherent limitation of in<T>. It is how it it used a certain piece of code.
2. if the benchmark is not doing what I think it is doing, any suggestions to improve it? I think it shows noticeable (1-2%) difference. I not passing any judgement on whether it is "worth" it. Statistically, sufficiently large number of iterations should make the result reliable even though no two runs are the same.
3. a library proposal and a language extension proposal for sink arguments could live in parallel. Possibly learning and gaining momentum from each other.

Thanks,
Sumant

Billy O'Neal

unread,
Sep 16, 2013, 7:29:28 PM9/16/13
to std-proposals
>Not if Richard's NB comment 12 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3733.pdf, search for "US 12") is properly resolved - which allows copy / move elision from xvalues.
 
Fair enough. :) But at the moment it isn't resolved (and isn't resolved for C++11), so I'm going to keep giving that advice until it changes.
 
>The point of the add_scalar example is, well, an example. Something to chew on while I'm trying to get the value of in<T> across as I see it. Possible overshadowing of move optimizations within that function are tangential to the main discussion.
 
No, they aren't tangential to that discussion at all. If you want the proposal to be taken seriously, you need to show a serious case where it makes a positive difference to clarity and/or performance or has some other benefit. At the moment, you haven't shown an example that I'd consider "motivating". And by that I mean an example where an idiot like myself can't poke bunches of holes in 10 minutes or less. If a user can't consider using the type in<T> or sink<T> in a high quality library, then it probably isn't appropriate in the standard. But I'm not on the committee, so what do I know?
 
>in<T> gives a way to separate lvalues from rvalues at run-time and I'm trying to solicit feedback on a pure library-based approach.
 
Yes, and I am giving you my feedback. That unless you can show a real world use case where it actually is an improvement, I think it is a bad idea as presented; adding complexity for little to no gain in perf.
 
>1. in<T> may interact with exception safety and may possibly improve/degrade it. But that is not an inherent limitation of in<T>. It is how it it used a certain piece of code.
 
(see above). You have to show a good use to motivate its inclusion in the standard. Your original use case (passing things into a constructor) was a great use case, but again I don't think it is implementable. (If it is implementable, that would be awesome)
 
>2. if the benchmark is not doing what I think it is doing, any suggestions to improve it? I think it shows noticeable (1-2%) difference. I not passing any judgement on whether it is "worth" it. Statistically, sufficiently large number of iterations should make the result reliable even though no two runs are the same.
 
Make sure your compiler isn't inlining the whole thing and defeating the benchmark. You can put each candidate in a separate function and ask your compiler never to inline that function (most compilers will allow this)
Don't use rand() in a benchmark -- make it a deterministic set of work that others can replicate to show your approach is superior.
Consider making the difference between a copy and a move for matrix larger to show a larger than 1-2% difference; e.g. pass around matrices with more elements.
Include the mechanism you use to measure time in your benchmark, and use the highest resolution clock available on your platform. (e.g. C++11 high_resolution_clock; QueryPerformanceCounter on Windows).
Run the benchmark multiple times in the same program, and discard the first couple of runs of each candidate solution to eliminate effects of initial code loading and similar.
 
Writing good benchmarks is really Really REALLY hard, particularly for micro-optimization cases like this.
 
===================================
 
How about an example like this?
 
class Foo
{
    optional<string> member; // Optional allows the data to be uninitialized.
public:
    Foo(in<string> str)
    {
        if (str.is_rvalue())
        {
            member.emplace(str.rget()); // move construct string
        }
        else
        {
            member.emplace(str.lget()); // copy construct string
        }
    }
 
    void MemberFunction()
    {
        // use member
        std::cout << *member;
    }
};
 
Unfortunately for more than one argument this is still not exception safe :(. On the other hand, when you have more than one parameter, the potential for collision with the special member functions goes away, so the argument against using perfect forwarding instead is much less strongly in favor of in<T>.
 
Billy O'Neal
Malware Response Instructor - BleepingComputer.com


Alex B

unread,
Sep 17, 2013, 9:35:55 AM9/17/13
to std-pr...@isocpp.org
>Not if Richard's NB comment 12 (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3733.pdf, search for "US 12") is properly resolved - which allows copy / move elision from xvalues.
 
Fair enough. :) But at the moment it isn't resolved (and isn't resolved for C++11), so I'm going to keep giving that advice until it changes.
 

Indeed, my example would not be optimal with current C++11. My point was mainly to point out that concepts will solve most of the problem.

If we want to explore in which way we can make it optimal, first remember that we are talking about getting features for at least C++17. I think we can assume there will be an optimal way when we get there. It could be either the NB comment Xeo mentionned or something else. For example, static if and/or static conditonnal operator could help here. It could result in something like this:

template <Sink<matrix> A, Sink<matrix> B>
matrix operator+(A a, B b)
{
   return is_mutable_rvalue<A> static ? move(a += b) :
          is_mutable_rvalue<B> static ? move(b += a) :
          matrix(a) += b;
}

See this thread about static if:

Alex B

unread,
Sep 19, 2013, 3:16:56 PM9/19/13
to std-pr...@isocpp.org
Something worth mentionning: unless I missed something, none of the solutions (including mine using concepts) is able to solve the problem when dealing with *this in a member function.

struct A
{
   void member() &;
   void member() const &;
   void member() &&;
};

zwv...@msn.com

unread,
Sep 20, 2013, 9:02:00 PM9/20/13
to std-pr...@isocpp.org

Maybe we should resurrect the idea T&&&.
There was once a C++0x paper about this syntax form which is a generalized one for the now-called universal reference. Not only can it solve the sink argument issue here, but also it can solve the problem when dealing with *this in a member function.
The syntax form may not be important. Perhaps T++ will also do. But we do need a generalized universal reference for any type, whether templatized or not.

Billy O'Neal

unread,
Sep 20, 2013, 10:48:25 PM9/20/13
to std-proposals
T&&& would have still required that the function be a template, and was functionally equivalent to what is now T&&.
 
I don't think solving this for member functions is a big deal. Have you ever been in a situation where you wanted to "generically copy or steal the guts from" this?

Billy O'Neal
Malware Response Instructor - BleepingComputer.com


--

Vittorio Romeo

unread,
Sep 19, 2014, 10:35:18 AM9/19/14
to std-pr...@isocpp.org
I'm resurrecting this after the awesome talk Herb Sutter gave at CppCon 2014, which covered in depth the issue of "sink" parameters.
In the talk, he showed benchmarks on the various "conventions" used to pass sink parameters.

Passing by value, then moving or passing by const reference can become very inefficient in some situations.

The known solutions are either creating every possible 2^n combination of parameters or using a template + perfect forwarding.
Both of these solutions are problematic: the first one requires an incredible amount of code duplication, the second one forces the programmer to use templates and it's hard to get right.

I can't help but feel that this is something that needs to be fixed in the language.

Therefore I, again, propose a new syntactic sugar argument-passing symbol that generates code equivalent to the 2^n solution.

Writing

struct Example {
    std
::string s1, s2;
   
Example(sink std::string mS1, sink std::string mS2) : s1{mS1}, s2{mS2} { }
};

is equvalent to writing

struct Example {
    std
::string s1, s2;
   
Example(std::string&& mS1,      std::string&& mS2)      : s1{std::move(mS1)}, s2{std::move(mS2)} { }
   
Example(const std::string& mS1, std::string&& mS2)      : s1{mS1}, s2{std::move(mS2)} { }
   
Example(std::string&& mS1,      const std::string& mS2) : s1{std::move(mS1)}, s2{mS2} { }
   
Example(const std::string& mS1, const std::string& mS2) : s1{mS1}, s2{mS2} { }
};

The `sink` syntax is just an example - I'm sure it could be much better. `std::sink<T>` could be used, or T&&&, or something completely different.

Thoughts?
Is this issue being looked into by the language evolution team? Should I try writing an official proposal and submit it?

Geoffrey Romer

unread,
Sep 19, 2014, 5:16:32 PM9/19/14
to std-pr...@isocpp.org
On Fri, Sep 19, 2014 at 7:35 AM, Vittorio Romeo <vittorio....@gmail.com> wrote:
I'm resurrecting this after the awesome talk Herb Sutter gave at CppCon 2014, which covered in depth the issue of "sink" parameters.
In the talk, he showed benchmarks on the various "conventions" used to pass sink parameters.

Passing by value, then moving or passing by const reference can become very inefficient in some situations.

As far as I can recall from the talk, the "inefficient" cases for pass-by-value happen when the function moves its parameter somewhere else (rather than using it in-place), and either:
1. the parameter type has an expensive move constructor (e.g. std::array) and the move into the parameter cannot be elided, or
2. the argument is an lvalue, and the argument is large enough that copying it requires memory allocation, but small enough that the O(N) cost of copying doesn't dominate the O(1) const of allocation, and the final destination happens to already have enough memory allocated to hold the argument value.

Have I forgotten any?

The known solutions are either creating every possible 2^n combination of parameters or using a template + perfect forwarding.
Both of these solutions are problematic: the first one requires an incredible amount of code duplication, the second one forces the programmer to use templates and it's hard to get right.

The situations above have at least two things in common: they're fairly unusual, and switching to pass-by-reference will not solve their performance problems. If #1 is dominating the run-time of a hot loop, switching to pass-by-reference eliminates only one of the two expensive move constructor calls, whereas switching to a type with a cheap move constructor (e.g. unique_ptr) eliminates both of them. As for #2, if your hot loop is dominated by copying smallish values to destinations that already contain smallish values, you probably shouldn't be looking for ways to eliminate the allocation overhead of those copies, but for ways to eliminate the copying (e.g. string interning, reference-counting). In the unlikely event that all those copies really are logically necessary, the situation is so tightly 

If your hot loop is dominated by copying smallish values of _all N parameters of a single function_ to destinations that already contain smallish values, and N is greater than 2, I'm gonna go out on a limb and say you need to re-think the life choices that brought you to that pass.

[Credit where it's due: my thinking here is heavily influenced by a conversation with Chandler Carruth and a few others. Any mistakes are of course my own]
 

I can't help but feel that this is something that needs to be fixed in the language.

I can.

I fear this may be an uphill battle after Herb's talk, but I still think pass-by-value is a perfectly good default choice for 'sink' arguments, and I'm OK with the fact that you have to do a bit of extra work if you need to squeeze out that last increment of performance.



Therefore I, again, propose a new syntactic sugar argument-passing symbol that generates code equivalent to the 2^n solution.

There's one kind of code to which the above reasoning may not apply: utility libraries that will be reused without modification across many application domains (e.g. standard library implementations). In that situation, you may not know where the hot loop in any given application will be (although I suspect you could make some pretty good guesses), so you may need to optimize pervasively. However, I think it's reasonable to expect maintainers of such libraries to be able to implement perfect forwarding successfully, and/or to just suck it up and generate the 2^N overloads. This group of C++ users is very small, but massively over-represented in places like CppCon and ISO, so we should be wary of giving excessive weight to their particular concerns. 
 


Writing

struct Example {
    std
::string s1, s2;

   
Example(sink std::string mS1, sink std::string mS2) : s1{mS1}, s2{mS2} { }
};

is equvalent to writing

struct Example {
    std
::string s1, s2;
   
Example(std::string&& mS1,      std::string&& mS2)      : s1{std::move(mS1)}, s2{std::move(mS2)} { }
   
Example(const std::string& mS1, std::string&& mS2)      : s1{mS1}, s2{std::move(mS2)} { }
   
Example(std::string&& mS1,      const std::string& mS2) : s1{std::move(mS1)}, s2{mS2} { }
   
Example(const std::string& mS1, const std::string& mS2) : s1{mS1}, s2{mS2} { }
};

The `sink` syntax is just an example - I'm sure it could be much better. `std::sink<T>` could be used, or T&&&, or something completely different.

Thoughts?
Is this issue being looked into by the language evolution team? Should I try writing an official proposal and submit it?

--

David Krauss

unread,
Sep 19, 2014, 8:43:06 PM9/19/14
to std-pr...@isocpp.org
On 2014–09–19, at 10:35 PM, Vittorio Romeo <vittorio....@gmail.com> wrote:

I'm resurrecting this after the awesome talk Herb Sutter gave at CppCon 2014, which covered in depth the issue of "sink" parameters.
In the talk, he showed benchmarks on the various "conventions" used to pass sink parameters.

What conclusion or recommendation was made in the talk?

Passing by value, then moving or passing by const reference can become very inefficient in some situations.

Well, the goal is to avoid unnecessary copies. Passing by value is optimal as long as moving is inexpensive, which holds true in most cases except large aggregates (classes with lots of subobjects) like std::array<T, large_size>.

Passing by value has another downside though, which is that the value of the argument is moved immediately to the parameter as the argument is evaluated, so the same object can’t be used in two arguments.

struct foo {
    std::unique_ptr< bar > up;
    std::shared_ptr< bar > sp;
};

void f( foo obj, std::shared_ptr< bar > mod ); // does not use obj.sp

void use( foo obj ) {
    f( std::move( obj ), obj.sp ); // Unspecified behavior: obj.sp may or may not be moved-from.
}

Solving this problem requires either more overloads, or a complicated policy in f such as guessing that the subobject should be used if the parameter is apparently moved-from, or (worst of all) requiring the user to make a copy of the subobject before passing.

Additional overloads are the only good solution, but users might be pushed into the other alternatives in trying to avoid changing a documented interface and ABI. A pass by value function can’t coexist as a legacy overload with pass by reference because it will produce overload ambiguity.

The known solutions are either creating every possible 2^n combination of parameters or using a template + perfect forwarding.
Both of these solutions are problematic: the first one requires an incredible amount of code duplication, the second one forces the programmer to use templates and it's hard to get right.

It requires std::forward. What’s hard about it?

Given that the language + library have a solution already, it might be better to build on that than to start from scratch.

I can't help but feel that this is something that needs to be fixed in the language.

Therefore I, again, propose a new syntactic sugar argument-passing symbol that generates code equivalent to the 2^n solution.

See the recent thread “templating qualifiers,” which ran August 27-30.

struct Example {
    std::string s1, s2;
    Example(sink std::string mS1, sink std::string mS2) : s1{mS1}, s2{mS2} { }
};

Brace-initialization isn’t enough to invoke a move. You need something that definitely signals to the reader, “this object gets invalidated!”

The `sink` syntax is just an example - I'm sure it could be much better. `std::sink<T>` could be used, or T&&&, or something completely different.

My suggestion (so far) is

template< typename std::string const & : && : sink_string1,
          typename std::string const & : && : sink_string2 >
Example( sink_string1 mS1, sink_string2 mS2 )
    : s1( std::forward< sink_string1 >( mS1 ) )
    , s2( std::forward< sink_string2 >( mS2 ) )
    {}

Still needs critique, refinement, and formal analysis.

Thoughts?
Is this issue being looked into by the language evolution team?

No current progress was mentioned yet in the other thread. I’d be interested to know, too.

Should I try writing an official proposal and submit it?

Sure, get the ball rolling!

Thiago Macieira

unread,
Sep 19, 2014, 10:18:25 PM9/19/14
to std-pr...@isocpp.org
On Saturday 20 September 2014 08:42:48 David Krauss wrote:
> Well, the goal is to avoid unnecessary copies. Passing by value is optimal
> as long as moving is inexpensive, which holds true in most cases except
> large aggregates (classes with lots of subobjects) like std::array<T,
> large_size>.

Where "large aggregates" is 16 bytes.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

David Krauss

unread,
Sep 20, 2014, 12:43:17 AM9/20/14
to std-pr...@isocpp.org

On 2014–09–20, at 10:18 AM, Thiago Macieira <thi...@macieira.org> wrote:

> On Saturday 20 September 2014 08:42:48 David Krauss wrote:
>> Well, the goal is to avoid unnecessary copies. Passing by value is optimal
>> as long as moving is inexpensive, which holds true in most cases except
>> large aggregates (classes with lots of subobjects) like std::array<T,
>> large_size>.
>
> Where "large aggregates" is 16 bytes.

16 bytes will pass in registers on a 64-bit system in the common ABI, so I don’t think that’s a good rule of thumb. Two registers are slower than one, but outside any performance-sensitive critical path, it doesn’t matter if you’re copying 100 bytes. Any memory access is dog-slow if you’re not reading from cache.

Convenience and micro-optimization don’t go hand in hand. Suffering the inconvenience of adding all the reference overloads, all the time, just for performance, is one’s own fault. If it weren’t for the correctness issue I mentioned, I’d see a lot less motivation for any new feature.

David Krauss

unread,
Sep 20, 2014, 12:46:34 AM9/20/14
to std-pr...@isocpp.org

On 2014–09–20, at 12:42 PM, David Krauss <pot...@gmail.com> wrote:

Convenience and micro-optimization don’t go hand in hand. Suffering the inconvenience of adding all the reference overloads, all the time, just for performance, is one’s own fault. If it weren’t for the correctness issue I mentioned, I’d see a lot less motivation for any new feature.

… And, for the sake of argument, there are other solutions to the moved-argument issue, such as specifying that any such move-initialization should be evaluated last. This would have the great advantage of fixing legacy code, albeit probably not 100% of the time.

Thiago Macieira

unread,
Sep 20, 2014, 1:46:18 AM9/20/14
to std-pr...@isocpp.org
On Saturday 20 September 2014 12:42:57 David Krauss wrote:
> > On Saturday 20 September 2014 08:42:48 David Krauss wrote:
> >> Well, the goal is to avoid unnecessary copies. Passing by value is
> >> optimal
> >> as long as moving is inexpensive, which holds true in most cases except
> >> large aggregates (classes with lots of subobjects) like std::array<T,
> >> large_size>.
> >
> >
> >
> > Where "large aggregates" is 16 bytes.
>
> 16 bytes will pass in registers on a 64-bit system in the common ABI, so I
> don't think that's a good rule of thumb. Two registers are slower than one,
> but outside any performance-sensitive critical path, it doesn't matter if
> you're copying 100 bytes. Any memory access is dog-slow if you're not
> reading from cache.

Which is why you should avoid passing by value anything that requires doing
copies to temporary memory due to the ABI. Don't pass by value anything larger
than 16 bytes or which is not trivially copyable.

David Krauss

unread,
Sep 20, 2014, 2:47:45 AM9/20/14
to std-pr...@isocpp.org

On 2014–09–20, at 1:46 PM, Thiago Macieira <thi...@macieira.org> wrote:

Which is why you should avoid passing by value anything that requires doing
copies to temporary memory due to the ABI. Don't pass by value anything larger
than 16 bytes or which is not trivially copyable.

The mere fact of copying 24 bytes does not imply bad performance. Duplicating large functions because the style guide says not to allow such copies will likely have a worse performance impact, due to bloat. And with deep inlining, it can be hard to predict what will ultimately be a large function.

Some level of judgment is required, and I prefer to add overloads as they become necessary, for correctness or performance.

Also note, my example gains a potential aliasing issue if the shared_ptr subobject is passed by reference. The callee probably expects to be able to move from one of its parameters without invalidating the other. I’ve been bitten by this.

Sean Middleditch

unread,
Sep 20, 2014, 2:42:38 PM9/20/14
to std-pr...@isocpp.org
On Friday, September 19, 2014 5:43:06 PM UTC-7, David Krauss wrote:

On 2014–09–19, at 10:35 PM, Vittorio Romeo <vittorio....@gmail.com> wrote:

I'm resurrecting this after the awesome talk Herb Sutter gave at CppCon 2014, which covered in depth the issue of "sink" parameters.
In the talk, he showed benchmarks on the various "conventions" used to pass sink parameters.

What conclusion or recommendation was made in the talk?


Trying my hand at paraphrasing what I took away (the talk slides are online, of course: see slide 25 in particular from https://github.com/CppCon/CppCon2014/tree/master/Presentations/Back%20to%20the%20Basics!%20Essentials%20of%20Modern%20C%2B%2B%20Style) the recommendation is to use the exact same parameter passing conventions common to C++98 but with the addition of using an rvalue-reference overload for expensive-to-copy/cheap-to-move types that the callee is planning to keep ownership of. Some other considerations and cases were brought up, the gist of the argument was to just do things like you did in C++98 and to exceedingly rarely take things by value.

I have counter-points, but most of them are based on (IMO) safer container designs than the STL uses (e.g. never ever allow implicit copying of an expensive-to-copy type), so they're rather irrelevant to this conversation I think.

Thiago Macieira

unread,
Sep 20, 2014, 3:00:08 PM9/20/14
to std-pr...@isocpp.org
On Saturday 20 September 2014 14:47:26 David Krauss wrote:
> The mere fact of copying 24 bytes does not imply bad performance.
> Duplicating large functions because the style guide says not to allow such
> copies will likely have a worse performance impact, due to bloat. And with
> deep inlining, it can be hard to predict what will ultimately be a large
> function.
>
> Some level of judgment is required, and I prefer to add overloads as they
> become necessary, for correctness or performance.
>
> Also note, my example gains a potential aliasing issue if the shared_ptr
> subobject is passed by reference. The callee probably expects to be able to
> move from one of its parameters without invalidating the other. I've been
> bitten by this.

Maybe we're talking about different things.

I'm mostly thinking of non-inlineable function calls because the function's
body is not available.

David Krauss

unread,
Sep 21, 2014, 6:11:19 PM9/21/14
to std-pr...@isocpp.org
On 2014–09–21, at 2:42 AM, Sean Middleditch <sean.mid...@gmail.com> wrote:

the recommendation is to use the exact same parameter passing conventions common to C++98 but with the addition of using an rvalue-reference overload for expensive-to-copy/cheap-to-move types that the callee is planning to keep ownership of.

Pages 23-26 make me queazy. Correctness takes precedence over performance in parameter passing semantics, as in anything else. The question is whether a function can safely peek and poke at your argument object, not whether you wish you didn’t have to copy it.

The “expensive to move => pass by lvalue reference” part sticks out like a sore thumb. Calling std::move() to pass by rvalue reference uses exactly the same ABI mechanics as passing by lvalue reference, and does not incur a move construction.

And “f(X) & move … always incurs a full copy…” is wrong. I’m not sure where std::move() can be applied that makes that correct.

Some other considerations and cases were brought up, the gist of the argument was to just do things like you did in C++98 and to exceedingly rarely take things by value.

It sounds like I would’ve had to be there. The slides alone aren’t particularly convincing. He benchmarked member assignment instead of initialization, then quoted Hinnant saying “Don’t blindly assume that the cost of construction is the same as assignment.” The SFINAE in “option 4” seems backwards and excludes the char* test, but I think he just didn’t actually implement it in testing. We really don’t see what it is he benchmarked.

Copying a const& parameter to a member is the same operation as copying an lvalue argument to a pass-by-value parameter object. There’s no way that moving the parameter into the member is expensive enough to make the difference shown by the graphs. If he substituted std::swap for move-assignment, would that be as expensive?

I have counter-points, but most of them are based on (IMO) safer container designs than the STL uses (e.g. never ever allow implicit copying of an expensive-to-copy type), so they're rather irrelevant to this conversation I think.

Well… we’re all pretty much in agreement that we want a way to generate reference overloads without hacking perfect forwarding. The question of why or when isn’t really as important as how. I sort of unfortunately hijacked the conversation by questioning the stated motivation, when really I only wanted to add an additional motivation regarding correctness, and mention my idea for the syntax.

But, I’m surprised that pass-by-value is out of vogue now, so this is interesting to me at least.

Sean Middleditch

unread,
Sep 21, 2014, 7:27:14 PM9/21/14
to std-pr...@isocpp.org
On Sun, Sep 21, 2014 at 12:50 AM, David Krauss <pot...@gmail.com> wrote:
> The “expensive to move => pass by lvalue reference” part sticks out like a
> sore thumb. Calling std::move() to pass by rvalue reference uses exactly the
> same ABI mechanics as passing by lvalue reference, and does not incur a move
> construction.
>
> And “f(X) & move … always incurs a full copy…” is wrong. I’m not sure where
> std::move() can be applied that makes that correct.
...
> Copying a const& parameter to a member is the same operation as copying an
> lvalue argument to a pass-by-value parameter object. There’s no way that

The example used iirc was a large std::array and in general applies to
any type which isn't movable but is copyable (and is expensive to
copy).
--
Sean Middleditch
http://seanmiddleditch.com

Andy Prowl

unread,
Oct 7, 2014, 3:50:16 PM10/7/14
to std-pr...@isocpp.org
I *think* constrained perfect-forwarding will be made easier with Concepts TS. For instance, it would be possible to define a concept like the following one:

template<typename T, typename U>
concept bool IsConvertible = requires()
{
    requires std
::is_convertible<U, std::decay_t<T>>{};
};

Thanks to terse notation ("abbreviated functions" using the proposal's terminology), we could then write functions this way:

void foo(IsConvertible<std::string>&&s) { /* ... */ }

The compiler will transform this into:

template<typename T>
requires
IsConvertible<T, std::string>
void foo(T&& s) { /* ... */ }

Of course the function would be a template, so the definition must appear in the header file, but at least the notation will be more readable and easier to get right.

Kind regards,

Andy

Vittorio Romeo

unread,
Oct 12, 2014, 9:46:54 AM10/12/14
to std-pr...@isocpp.org
I've written a draft for a proposal (first time): https://gist.github.com/SuperV1234/7a945b7fa0895bb2ea3c

Is it acceptable? What should be added/changed/removed?

Ville Voutilainen

unread,
Oct 12, 2014, 9:56:29 AM10/12/14
to std-pr...@isocpp.org
I think it would be reasonable to explain what is different from
http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3538.html
and why.

David Krauss

unread,
Oct 12, 2014, 10:21:39 AM10/12/14
to std-pr...@isocpp.org

On 2014–10–12, at 9:56 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

> I think it would be reasonable to explain what is different from
> http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3538.html
> and why.

One offers a const reference OR value, the other offers a const reference AND rvalue reference.

I think it would be nice to have a general facility for pseudo-template permutations (well, just combinatorial alternatives actually), with optional syntactic sugar to avoid the template parameter declaration where it could be implicit, and an exception to overload resolution that lets the compiler choose if two alternatives tie for the best match.

Perhaps the syntactic sugar isn’t strong enough to allow the generated functions to be non-inline, though, which should help stem the combinatorial explosion.

See the recent thread, “templating qualifiers” opened by Matthew Fioravante on Aug 27. The idea still needs development, but it could be more general than just a particular sink idiom.
Reply all
Reply to author
Forward
0 new messages