Alex Shtoff
Alex Shtoff

Reputation: 2640

Move semantics and virtual methods

In C++11 we are guided in some cases to pass objects by value and in others by const-reference. However, this guideline depends on the implementation of the method, not just on its interface and intended usage by its clients.

When I write an interface, I do not know how it will be implemented. Is there a good rule of thumb for writing method signatures? For example - in the following code fragment, should I use Bar1 or Bar2?

class IFoo
{
public:
    virtual void Bar1(std::string s) = 0;
    virtual void Bar2(const std::string& s) = 0;
};

You can stop reading here if you agree that the correct signature depends on the implementation. Here is an example that shows why I believe so.

In the following example, we should pass the string by value:

class Foo
{
    std::string bar;

    Foo(std::string byValue)
        : bar(std::move(byValue))
    {
    }
};

Now we can instantiate Foo in an efficient manner in all cases:

Foo foo1("Hello world"); // create once, move once
Foo foo2(s); // the programmer wants to copy s. One copy and one move
Foo foo3(std::move(t)); // the programmer does not need t anymore. No copy at all

In other cases we prefer to pass objects by const reference. For example, in the following case we never want to copy/store the argument, just use its methods:

void DoStuff(const std::string& byRef)
{
    std::cout << byRef.length() << std::endl;
}

All possible usages of the above method are already as efficient as possible.

Update

I believe I forgot to show the issues with the const-reference alternative. If the above class Foo was implemented this way:

class Foo
{
    std::string bar;

    Foo(const std::string& byRef)
        : bar(byRef)
    {
    }
};

Then we would have the following results:

Foo foo1("Hello world"); // Here we would have one more copy of the string. It is less efficient.
Foo foo2(s);             // One copy, like before
Foo foo3(std::move(t));  // Irrelevant here.

Alex.

Upvotes: 12

Views: 1421

Answers (3)

fredoverflow
fredoverflow

Reputation: 263138

You could also provide an overload for Bar2 that takes an rvalue reference:

class IFoo
{
public:
    virtual void Bar2(const std::string& s) = 0;

    virtual void Bar2(std::string&& s)
    {
        Bar2(s);   // calls the const& overload because s is an lvalue
    }
};

By default, the rvalue reference overload simply calls the const lvalue reference overlad. But if a specific subclass can take advantage of rvalue references, the rvalue reference overload can be overriden.

Upvotes: 2

lutzky
lutzky

Reputation: 618

I believe it should definitely depend on the implementation. As implied from your question, barring a completely "always-better" signature, the only sensible thing to do is to choose the signature in a manner that optimizes the current implementation. If you write the interface before the code - take an educated guess, and try to maneuver yourself in such a way that you can wait for a first implementation before you commit to the signature.

The operative words here are "first" and "current". What happens if you got it wrong? What happens if at some later stage the signature prevents your code from being optimal? Here's what you can do:

No commitment

If it's soon enough - just change it. It follows from the definition of "no commitment", right?

Committed to API

For a concrete example, assume you chose wrong, and went with this:

virtual void DoStuff(std::string s) = 0;

However, as it turns out, no copying needs to be performed (same as your original DoStuff implementation). Here's what you can do:

// stuff.h
virtual void DoStuff_Optimized(const std::string & s);
virtual void DoStuff(std::string s);

// stuff.cc
virtual void DoStuff_Optimized(const std::string & s);
{
    // Fast implementation of DoStuff, no copying necessary
    std::cout << s.length() << std::endl;
}

virtual void DoStuff(std::string s)
{
    DoStuff_Optimized(s);
}

Existing clients will get inferior performance. New clients can use the Optimized version.

Committed to ABI

There might be nothing you can do at this point, unfortunately. However, if you're careful, you might be able to follow the "Committed to API" action. (In particular, my example will not preserve ABI compatibility).

Upvotes: 0

Yam Marcovic
Yam Marcovic

Reputation: 8141

There's no "theory of everything" here. You got it right, there's a problem. I remember confronting it myself a while back.

My conclusions started here:

Application vs. Framework/Library Development

If your clients are developers, this job is much harder. Not only is it harder, but there are no clear guidelines. Great framework designers got their prestige because they happened to take risks that paid off. At the same time, in an alternate universe, their risks could have not paid off. That's because appreciating a framework depends on the direction of its growing usage, and subjective opinions which are much harder to reason about than in the application domain.

So there's no clear cut answer in this case. Fortunately, I think you're interested mainly in Application development here. So let's get on to that.

Starting point: We're developing applications

This makes a huge difference. Because we're supposed to have a much better idea of where the system is going, and what kind of code could turn out to be useful. We're not prophets, but at the same time this assumption allows us to give more credit to our intuition, which is based on our knowledge of the requirements, and the needs of our customers (at least as much as we were able to understand).

At this point, we can still divide this into 2 cases:

Abstraction to Implementation

There are cases where it is beneficial, or even necessary, to define abstraction ahead of the implementation. In cases like this, one has to realize that much more research about the problem is required before defining the abstraction properly. For example, is the domain synchronous or asynchronous? Serial or parallel? High or low level? And other much more concrete questions.

Some extreme agilers will have you believe that you can just write some code and fix it later. However, that claim is falsified very easily once reality hits. If you find hope in it, I encourage you to test it yourself and report if you made any significant discovery. My personal experience, and thought that I have tried putting into the matter, suggest that in big projects this approach is very problematic.

The conclusion in this case is that, if you indeed need to define abstraction ahead, then you should already have a very good idea of the implementation. The better idea you have about it, the higher the chance it will succeed in actually being a proper abstraction.

Implementation to Abstraction

This is my default choice. It has been said in many ways. "Frameworks should be extracted", "Extract 'till you drop", and even "Convention over Configuration" has some similarities in concept.

Basically this means that you implement your required components as necessary, but keep a sharp eye on what's going on. The trick here is to look out for chances to abstract in ways that actually benefit you practically in terms of development and maintenance.

This often comes up as a class that does what you want, but more. In which case, you abstract the intersection away into a more general case. You repeat this process as necessary throughout development.

It's important to not get caught up and still keep your feet on the ground. I've seen many abstraction attempts go wrong to a point where there's no way to reason about its name and deduce its intent except reading thousands of lines of code that use it. For example, in the current code base I'm working on, the type which should have been called Image is called BinaryData. All across the code are attempts to treat it as a concrete (Image), and as an abstract concept at the same time.

To sum up

As I always remind myself, the best best practice you can have is to tame known best practices to fit your problem, rather than the other way around. If you can't do that, well, maybe the problem is interesting enough to require further attention, and a bit of original thought.

Upvotes: 4

Related Questions