What's the correct `enable_if` constraint on perfect forwarding setter?

前端 未结 3 1778
孤街浪徒
孤街浪徒 2020-12-28 19:05

Herb Sutter\'s Back to the Basics! Essentials of Modern C++ presentation at CppCon discussed different options for passing parameters and compared their performance

相关标签:
3条回答
  • 2020-12-28 19:09

    I tried to fit these thoughts in a comment, but they would not fit. I presume to write this as I am mentioned both in the comments above, and in Herb's great talk.

    Sorry to be late to the party. I have reviewed my notes and indeed I am guilty of recommending to Herb the original constraint for option 4 minus the errant !. Nobody is perfect, as my wife will assuredly confirm, least of all me. :-)

    Reminder: Herb's point (which I agree with) is start with the simple C++98/03 advice

    set_name(const string& name);
    

    and move from there only as needed. Option #4 is quite a bit of movement. If we are considering option #4, we are down to counting loads, stores and allocations in a critical part of the application. We need to assign name to name_ just as fast as possible. If we are here, code readability is far less important than performance, though correctness is still king.

    Providing no constraint at all (for option #4) I believe to be slightly incorrect. If some other code attempted to constrain itself with whether or not it could call employee::set_name, it could get the wrong answer if set_name is not constrained at all:

    template <class String>
    auto foo(employee& e, String&& name) 
    -> decltype(e.set_name(std::forward<String>(name)), void()) {
        e.set_name(std::forward<String>(name));
        // ...
    

    If set_name is unconstrained and String deduces to some completely unrelated type X, the above constraint on foo improperly includes this instantiation of foo in the overload set. And correctness is still king...

    What if we want to assign a single character to name_? Say A. Should it be allowed? Should it be wicked fast?

    e.set_name('A');
    

    Well, why not?! std::string has just such an assignment operator:

    basic_string& operator=(value_type c);
    

    But note that there is no corresponding constructor:

    basic_string(value_type c);  // This does not exist
    

    Therefore is_convertible<char, string>{} is false, but is_assignable<string, char>{} is true.

    It is not a logic error to try to set the name of a string with a char (unless you want to add documentation to employee that says so). So even though the original C++98/03 implementation did not allow the syntax:

    e.set_name('A');
    

    It did allow the same logical operation in a less efficient manner:

    e.set_name(std::string(1, 'A'));
    

    And we are dealing with option #4 because we are desperate to optimize this thing to the greatest degree possible.

    For these reasons I think is_assignable is the best trait to constrain this function. And stylistically I find Barry's technique for spelling this constraint quite acceptable. Therefore this is where my vote goes.

    Also note that employee and std::string here are just examples in Herb's talk. They are stand-ins for the types you deal with in your code. This advice is intended to generalize to the code that you have to deal with.

    0 讨论(0)
  • 2020-12-28 19:12

    I think what you have is probably right, but in the interest of not writing an "answer" that is simply "I agree", I will propose this instead that will check assignment based on the correct types - whether it's an lval, rval, const, whatever:

    template <class String>
    auto set_name(String&& name) 
    -> decltype(name_ = std::forward<String>(name), void()) {
        name_ = std::forward<String>(name);
    }
    
    0 讨论(0)
  • 2020-12-28 19:16

    One explanation I considered was that there's a simple typo and the constraint was intended to be std::is_same<std::decay_t<String>, std::string>::value instead of !std::is_same<std::decay_t<String>, std::string>::value.

    Yes, the right constraint that was presented on the screen was is_same, not !is_same. Looks like there is a typo on your slide.


    However that would imply that the setter doesn't work with, e.g., const char *

    Yes, and I believe this was done on purpose. When a string literal like "foo" is passed to a function accepting a universal reference, then the deduced type is not a pointer (since arrays decay to pointers only when caught in template parameter by value), rather, it is a const char(&)[N]. That said, every call to set_name with string literal of different length would instantiate a new set_name specialization, like:

    void set_name(const char (&name)[4]); // set_name("foo");
    void set_name(const char (&name)[5]); // set_name("foof");
    void set_name(const char (&name)[7]); // set_name("foofoo");
    

    The constraint is supposed to define the universal reference so that it accepts and deduces only either std::string types for rvalue arguments, or cv-std::string& for lvalue arguments (that's why it is std::decayed before comparing to std::string in that std::is_same condition).


    it obviously was intended to work with this type given that that's one of the cases tested in the presentation.

    I think the tested version (4) was not constrained (note it was named String&&+perfect forwarding), so it could be as simple as:

    template <typename String>
    void set_name(String&& name)
    {
        _name = std::forward<String>(name);
    }
    

    so that when a string literal is passed, it does not construct the std::string instance prior to function call just like the non-templated versions would do (unnecessarily allocating memory in callee's code just to build a std::string temporary that would be eventually moved to possibly preallocated destination like _name):

    void set_name(const std::string& name);
    void set_name(std::string&& name);
    

    It seems to me that the correct constraint is more like: std::enable_if_t<std::is_assignable<decltype((name_)), String>::value>>

    No, as I wrote, I don't think the intent was to constrain the set_name to accept types assignable to std::string. Once again - that original std::enable_if is there to have a single implementation of set_name function taking a universal reference accepting only std::string's rvalues and lvalues (and nothing more although this is a template). In your version of std::enable_if passing anything non-assignable to std::string would yield an error no matter if the constaint is or not when tried to do so. Note that eventually we might be just moving that name argument to _name if it's non-const rvalue reference, so checking assignability is pointless except from situations when we are not using a SFINAE to exclude that function from overload resolution in favor of other overload.


    What's the correct enable_if constraint on perfect forwarding setter?

    template <class String,
              class = std::enable_if_t<std::is_same<std::decay_t<String>,
                                                    std::string>::value>>
    

    or no constraint at all if it won't cause any performance hit (just like passing string literals).

    0 讨论(0)
提交回复
热议问题