Last Wednesday evening, I gave a talk at the Northwest C++ Users' Group entitled "The Universal Reference/Overloading Collision Conundrum." The purpose of the talk was to try out the information behind a guideline from Effective C++11/14 (the book I'm currently working on). That guideline is "Avoid overloading on universal references." The video for that talk is now available.
From my perspective, the talk was a success, but that may not be apparent from the video. Things went fine for the first 12 minutes, and then...things went less fine. Bugs in the slides. Questions I didn't answer. Material I didn't have time to cover. All of which may--should--make you wonder how I define "success."
As a general rule, I like to test material in front of live audiences before I put it in my books. Presenting technical material live is perhaps the best way to get feedback on it. Not only does it offer attendees an opportunity to ask questions and make comments (direct feedback), it gives me a chance to see people's reactions (indirect feedback). Even if an audience asks no questions and makes no comments, looking into their faces tells me if they're engaged or bored and if they're following what I'm saying or are confused. Plus, the simple act of explaining something gives me a chance to see how well it flows in practice. It's quite common for me to think to myself "this just isn't working the way I'd hoped..." while I'm speaking, and places where I think that identify parts of the presentation where I need to go back and make revisions.
From the presentation at the NWC++UG (including some conversations I had with attendees afterwards), I took away two primary lessons. First, the guideline I was presenting ("Avoid overloading on universal references") is both valid and useful. That was reassuring. Second, the technical justification I give for this guideline needs a fair amount of work. In particular, I need to avoid getting side-tracked too much by the issues surrounding overloading on universal references and its interaction with compiler-generated special functions. Both lesson will help me produce a better book, and that's why I consider the talk a success.
At the same time, I was disappointed that there were bugs in my slides. I have pretty much a zero-tolerance mindset for errors in presentation materials (as well as books and articles and other forms of publication), because authors (including me) have essentially unlimited amounts of time to prepare the materials prior to making them public. (If there's insufficient time to prepare the materials properly, my feeling is that you shouldn't agree to present or publish them.) To be honest, I was also surprised that my materials had the errors that they did, because I hadn't skimped on prep time or QA work. I really thought they were ready to go. I was mistaken. In the future, I'll clearly have to find ways to do a better job.
Since giving the talk, I've corrected and revised the materials, and the corrected slide set is available here.
I hope you enjoy the talk, rocky parts notwithstanding.
Scott
The typo of writing Widget for the template parameter instead of T was something you also did when you first presented this material on your blog here. And in both cases, it wasn't immediately spotted by the audience.
ReplyDeleteI would therefore speculate that both the audiences and you perform some kind of two-phase parsing when reading the code. "No template list + class id as function name and parameter" -> regular copy constructor. "A template list + class id as function name" -> universal copy constructor. The 2nd phase only kicks in a little later: OOPS, template parameter list does match function parameter list.
Anyway, minor typos aside, it was an interesting talk!
Hello Scott,
ReplyDeleteI had asked a question on Twitter but realized it would be nearly impossible to answer a technical question in 140 characters. So I will re-ask the question here, where you have a better place to answer.
I had asked about how templates would not deduce f({ 1, 2, 3 }) as std::initializer_list? I'm very curious about that. I would have guessed that it would. I had guessed that some compiler magic just understood that in that case it was creating a temporary std::initializer_list, as a sort of syntactic sugar replacement for f( std::initializer_list{ 1, 2, 3 } ) or something along those lines.
Would you mind enlightening me?
If the Special Member Functions were deletes with Person(Person const&)=delete; etc., would the universal reference ctor then be called?
ReplyDelete@Craig Henderson: a function that's "=delete"d is still declared, so it participates in overload resolution. As a result, deleting the special member functions won't change anything.
ReplyDelete@ProgramMax: The Standard explicitly states that template type deduction doesn't word for braced initializers. (It's a "non-deduced context".) I have never been able to find out the reason for this. If you can find out why this decision was made, I'd be very happy to learn it from you!
ReplyDeleteI was wondering if in slide 9 you meant to use a forwarding constructor instead of initializing name directly from nameFromId. It's the same thing, but it would nicely chain the forwarding. Or explain how string constructor is implemented. Just a thought...
ReplyDelete@bartoszmilewski.com: nameFromID is known to return an rvalue (per slide 8), so it would make no sense to apply std::forward to it. std::forward is used with parameters that may have been initialized with an rvalue, but may also have been initializd with an lvalue. Or do you mean something else?
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteFrom my reading of the paper trail, the committee when back and forth on having f(T) deduce T to an initializer_list. I believe they were concerned about the ambiguity of:
ReplyDeletestruct X { X(int, int); };
void f(X);
template <class T>
void f(T);
f({1,2});
In any case, you can deduce a list with:
template <class T>
void f(std::initializer_list<T>);
if so desired.
@Casey: From my perspective, the question isn't why a type isn't deduced for braced initializers in a template context, it's why they aren't deduced for templates, yet they are deduced for auto. Your example provides a hint at the possible reason (auto doesn't need to worry about overload resolution), but just as auto sometimes deduces a type for a braced initializer that a programmer doesn't want, thus forcing the programmer to manually force a different type deduction, it's not clear to me that the same thing can't be done for templates.
ReplyDeleteI don't see type deduction (or the lack thereof) as the core issue, I see the discrepancy in type deduction rules between auto and templates as the fundamental matter.
One of the things I'd like to see addressed is proper handling of the copy constructor for non-const lvalue references. Someone in the audience references this near the end.
ReplyDeleteOne solution is to provide an explicit definition for Person(const Person &) *and* for Person(Person &), but that seems excessive.
What is your recommendation on this?
This is the code I'd like simplified, if possible. The universal constructor is used to create Person and I'd like to preserve default copy constructor semantics. X is declared to ensure Person is deep copied when expected:
ReplyDelete#include
struct X
{
X() { std::cout << "X()" << std::endl; }
X(const X &) { std::cout << "X(const X &)" << std::endl; }
};
struct Person
{
template
Person(T &&) {}
Person(Person & p) : Person(const_cast(p)) {}
Person(const Person &) = default;
X x;
};
int main()
{
Person p(1);
const Person cp(1);
Person{p};
Person{cp};
return 0;
}
@Anonymous: I'll respond to your question about handling non-const lvalues in a separate blog post, because I think it's a more complicated issue than it at first appears. My tentative conclusion is that you should avoid perfect-forwarding single-argument constructors (i.e., those taking only a universal reference), because they lead to a lot of trouble with the special functions. When this isn't possible or practical, getting the behavior you want from the copying and moving functions calls for more work than you'd think. As I said, I'll offer details in a blog post. That will probably come in a few days (i.e., in early August).
ReplyDeleteGreat! I look forward to it.
ReplyDeleteHi Scott,
ReplyDeleteI just wanted to thank you for battling through all of this! I am particularly looking forward to your new book to explain this with all the kinks ironed out.
Regards,
Ben
@Ben Hanson: Thanks, I'm looking forward to the book, too :-) I expect to finish the chapter on rvalue references, move semantics, and perfect forwarding (including the discussion of why it's ill-advised to overload on universal references) this week. That's the first chapter I'm writing, so it'll be one down, n to go!
ReplyDeleteRE: "From my perspective, the question isn't why a type isn't deduced for braced initializers in a template context, it's why they aren't deduced for templates, yet they are deduced for auto."
ReplyDeleteThe discussion in N2640, which was a reaction paper to the proposal in N2532, is the closest thing I've found to rationale. N2640 removed the deduction for template parameters from N2532 citing overloading surprises, but left in deduction for auto specifically citing:
auto x = { 1, 1, 2, 3, 5 };
f(x);
g(x);
as having been a long-term EWG goal use-case for initializer lists.
Given that earlier papers (N2532, N2477, N2215) had waffled on both deductions - but kept them consistent - I imagine N2640 was a compromise necessary to push though into the working paper.
Looking back on it now, the motivating example for allowing auto to deduce initializer_list seems very niche considering that it is more often used erroneously than by intention today. Hopefully N3681 will be accepted and we won't have to continue to teach people to avoid this particular landmine in the language.
@Casey: Thanks very much for sharing the results of your investigation into this matter!
ReplyDeleteScott,
ReplyDeletethanks for publishing the video of this talk! I've found it interesting material to watch and it made quite a lot of things clear to me. Do not hesitate to record other presentations you give!
Regards,
Bart
Scott,
ReplyDeleteIn your revised slide 23, the second "normal" copy ctor has std::move(rhs) and you explain that the code makes no sense.
Doesn't changing that to std::move(rhs.name) solve the problem and results in code that makes sense?
Thank you for sharing this very insightful talk!
ReplyDeleteInspired by the audience-question, I tried to compile the template non const ref/refref version like that:
#include
#include
class Widget {
public:
std::string name;
Widget(std::string n) : name(std::move(n)) {}
};
class MessedUp {
public:
template
void doWork(T& param) { std::cout << "T&: " << param.name << std::endl; }
template
void doWork(T&& param) { std::cout << "T&&: " << param.name << std::endl; }
};
int main() {
MessedUp m;
Widget w("w");
const Widget cw("cw");
m.doWork(w);
m.doWork(std::move(w));
m.doWork(cw);
m.doWork(std::move(cw));
return 0;
}
Unsurprisingly it failed in gcc(4.8.1) intel(13.0.1) and pgi(13.4) due to the ambiguous overload. However, clang(3.3) compiled it withouot warning, even with -pedantic. With -Weverything it complained only about rvalue references not beeing compatible with C++99. The output is the expected:
T&: w
T&&: w
T&: cw
Would you agree that this is a bug in clang?
T&&: cw
@Bart Vandewoestyne: In the slides I presented at the talk, I used "std::move(rhs.name)" in the copy constructor. This compiles and, in this case, it works, but, as I note in the revised slides, the idea of initializing a std::string with a Person doesn't generally make any sense. If there happened to be duplicated code in the copy and templatized constructors, I suppose that by having the copy constructor delegate to the templatized constructor, such duplication could be avoided, but a simpler way to accomplish the same thing would be for both constructors to call a common function.
ReplyDelete@Thomas: For rvalues, it seems clear that the universal reference overload should be called. For lvalues, I would have guessed that the lvalue reference overload would be called, because it's a more specialized template than the universal reference overload. VC12 calls the lvalue reference overload, though, as you note, g++ 4.8 flags those calls as ambiguous. I'm still not sure which behavior is correct, but if somebody is willing to plow through the standard to figure it out, I'd be pleased to know the answer.
ReplyDeleteThe T& vs. T&& overload issue just came up on Stackoverflow yesterday. 14.8.2.4/9 specifies that the lvalue overload is preferred when both match, it's a known bug that GCC believes they are ambiguous (I don't know about other compilers.)
ReplyDelete@Casey: Thanks for that link. And it looks like my guess was right! Who'd have seen that coming? :-)
ReplyDeleteEric Niebler touches on one way to protect against the copy constructor issues I raised (as Anonymous again, above):
ReplyDeletehttp://ericniebler.com/2013/08/07/universal-references-and-the-copy-constructo/
@Anonymous: I apologize for not following up on your question in a separate blog post, as I'd said I would. At the time, I was working on the book treatment of the guideline, "Avoid overloading on universal references," and by the time I finished it a few days later, one Item had grown into two (one to explain the problems arising from universal references, one to explain the possible ways to address them), and the Item containing the material you asked about was pushing 5000 words (roughly 15-20 pages).
ReplyDeleteFor the particular problem you asked about, I think Eric's approach is the most practical one. It happens to be the one I describe in book, too, but we came up with it independently, except for the part about using std::is_base_of instead of std::is_same to see that inheritance is handled correctly. Eric came up with that, and I got the idea from him.