perfect forwarding constructors

Started by
10 comments, last by SmkViper 8 years, 9 months ago

Can someone give me a convincing argument against always using a templated constructor that perfect forwards its arguments to initialize its member variables?

In the very simple example case:


    template<typename T>
    class SocketError : public std::exception {
    public:
        SocketError(T&& what_arg)
            : what_arg_(std::forward<T>(what_arg)) { }

        virtual const char* what() const noexcept override {
            std::cout << what_arg_;
        }
    private:
        std::string what_arg_;
    };

A more complicated case might use a variadic template or multiple universal reference constructor arguments.

I'm finding it hard to convince myself that this is a bad idea, so if anyone would like to enlighten me, please do.

Downsides I'm aware of:

Potentially increased size of the object code

Debugging can be a bitch

I'm aware that certain compiler optimizations may make this less valuable, but let's generalize here.

Advertisement

Always is a strong word. Let's say "most of the time" instead

Off the top of my head: 1) because it makes type mismatch error messages flag an error in the constructor body rather than the caller. 2) it increased compile times 3) it's completely pointless for primitive types. 4) it prevents you from putting the constructor body in a separately compiled source file 5) single argument universal reference constructors can have weird interactions with compiler generated copy constructors. In other words, sometimes you may think you're trying to copy your object but what you're actually going to do is try to initialize the member variable with a reference to your object. Actually it generally has poor interactions with any other overloads, but the compiler generated copy constructor is the one you'll see the most trouble with.

Off the top of my head: 1) because it makes type mismatch error messages flag an error in the constructor body rather than the caller. 2) it increased compile times 3) it's completely pointless for primitive types. 4) it prevents you from putting the constructor body in a separately compiled source file 5) single argument universal reference constructors can have weird interactions with compiler generated copy constructors. In other words, sometimes you may think you're trying to copy your object but what you're actually going to do is try to initialize the member variable with a reference to your object. Actually it generally has poor interactions with any other overloads, but the compiler generated copy constructor is the one you'll see the most trouble with.

These are valid concerns that I will take seriously. On a strictly idealistic level, in terms of runtime performance, these are nonissues (with the exception of the point about primitive types), right?

I see these concerns as motivation to design my debugging system in a way to reduce nonsense error messages, rather than motivation to not use perfect forwarded constructors.

There can be run time performance issues. When you use template forwarding arguments any conversion operations that need to occur will happen inside the function body rather than outside the function body. For instance, address adjustments when moving from derived to base pointer or references. This inhibits optimizations like redundant COMDAT folding done by MSVC. For that matter if your tool chain doesn't support code folding for functions with identical machine code but different symbol names, then passing different derived type pointer or reference arguments would cause code bloat even if address adjustments aren't necessary. Like most template bloat this can be firewalled to some extent by delegating as much work as possible to non-template code. In this case meaning that damage would be limited if your member variables don't have template constructors. However, your question is specifically about applying template constructors everywhere....

You're templating the whole class in your example. It's not feasible for every class to be templated in a large project...

You're templating the whole class in your example. It's not feasible for every class to be templated in a large project...

Yea, I realized that last night when reading SiCrane's most recent comment. That was a mistake. My question was about universal references, which the sample code does not contain. It should really look like this:


    class SocketError : public std::exception {
    public:
        template<typename T>
        SocketError(T&& what_arg)
            : what_arg_(std::forward<T>(what_arg)) { }

        virtual const char* what() const noexcept override {
            std::cout << what_arg_;
        }
    private:
        std::string what_arg_;
    };

That being said, even with the acute drawbacks mentioned by SiCrane, I'm still convinced that this is a good idea in a significant majority of cases. I sincerely appreciate SiCrane's noting of some niche cases, and I will have to continue learning about similar cases so that I can avoid them in practice. If anyone has any additional information to add, I'd love to hear it.

I wouldn't call causing copy constructors to fail a niche case. Especially since you can see the problem with the sample code you just posted:

SocketError a("a");
SocketError b(a);
With MSVC this produces the error: error C2664: 'std::basic_string<_Elem,_Traits,_Alloc>::basic_string(const std::basic_string<_Elem,_Traits,_Alloc> &)' : cannot convert parameter 1 from 'SocketError' to 'const std::basic_string<_Elem,_Traits,_Alloc> &'

Okay, I didn't contextualize that comment previously, but I think this definitely pushes this into the overkill category for the 1 arg constructor case. While I believe I could avoid this issue using expression SFINAE or a copy constructor with a targetted prototype, both solutions feel like they are trying to justify something that is simply not valuable enough to justify.

That being said, the implicitly generated assignment operator still works (not necessarily a good enough solution, but something to keep in mind in certain cases). Also, this problem is less potent for move-only objects.

I can't think of similar issues with multiple argument constructors, but, considering I didn't think of this issue in the first place, I might be missing something.

I don't want to abandon this paradigm solely because there are times when it may be costly and, therefore, not the right thing to do. Instead, I hope to become more aware of the set of potential issues with doing this. I do think that it offers some valuable, low cost efficiency gains in a lot of situations. Do you disagree with that?

Side note: the amount of errors in my tiny sample is embarassing (what() should return what_arg_.c_str() or something).

Lemmie make a suggestion:


class SocketError : public std::exception {
public:
    explicit SocketError(std::string what_arg)
        : what_arg_(std::move(what_arg)) { }

    virtual const char* what() const noexcept override {
        std::cout << what_arg_;
    }
private:
    std::string what_arg_;
};
(Explicit added to avoid silent conversions between strings and exceptions)

This should end up being almost as efficient (if not just as efficient depending on compiler optimizations) as your universal reference, but doesn't suffer the problems of a universal reference (greediness, poor error messages). Not to mention it's easier for your user to read, as they can see that you want a std::string and is not trying to guess what happens if they pass an integer.

If you pass in a temporary, then the std::string constructor will take over and the result will be moved into your member (no copies, one or two moves). If you pass in a non-temporary, then the std::string will copy into the parameter, and move into the member (one copy, one move).

Of course, this method isn't perfect, it'll slice if your parameter type is inherited from, makes things worse if your parameter doesn't have a cheap move, and may cause an extra construction if you use it for a non-constructor and assign, or only conditionally copy the value.

Reference: Effective Modern C++ (free excerpt on this exact topic)

This topic is closed to new replies.

Advertisement