0

Here is the problem I was thinking about lately. Let's say our interface is a member function that returns object which is expensive to copy and cheap to move (std::string, std::vector, et cetera). Some implementations may compute the result and return a temporary object while others may simply return a member object.

Sample code to illustrate:

// assume the interface is: Vec foo() const
// Vec is cheap to move but expensive to copy

struct RetMember {
    Vec foo() const { return m_data; }
    Vec m_data;
    // some other code
}

struct RetLocal {
    Vec foo() const {
        Vec local = /*some computation*/;
        return local;
    }
};

There are also various "clients". Some only read the data, some require an ownership.

void only_reads(const Vec&) { /* some code */ }
void requires_ownership(Vec) { /* some code */ }

Code above composes well, but is not as efficient as it could be. Here are all combinations:

RetMember retmem;
RetLocal retloc;

only_reads(retmem.foo()); // unnecessary copy, bad
only_reads(retloc.foo()); // no copy, good

requires_ownership(retmem.foo()); // copy, good
requires_ownership(retloc.foo()); // no copy, good

What is a good way to fix this situation?

I came up with two ways, but I'm sure there is a better solution.

In my first attempt I wrote a DelayedCopy wrapper that holds either a value of T or a pointer to const T. It is very ugly, requires extra effort, introduces redundant moves, gets in the way of copy elision and probably has many other problems.

My second thought was a continuation-passing style, which works quite well but turns member functions into member function templates. I know, there is std::function, but it has its overhead so performance-wise it may be unacceptable.

Sample code:

#include <boost/variant/variant.hpp>
#include <cstdio>
#include <iostream>
#include <type_traits>

struct Noisy {

  Noisy() = default;
  Noisy(const Noisy &) { std::puts("Noisy: copy ctor"); }
  Noisy(Noisy &&) { std::puts("Noisy: move ctor"); }

  Noisy &operator=(const Noisy &) {
    std::puts("Noisy: copy assign");
    return *this;
  }
  Noisy &operator=(Noisy &&) {
    std::puts("Noisy: move assign");
    return *this;
  }
};

template <typename T> struct Borrowed {
  explicit Borrowed(const T *ptr) : data_(ptr) {}
  const T *get() const { return data_; }

private:
  const T *data_;
};

template <typename T> struct DelayedCopy {
private:
  using Ptr = Borrowed<T>;
  boost::variant<Ptr, T> data_;

  static_assert(std::is_move_constructible<T>::value, "");
  static_assert(std::is_copy_constructible<T>::value, "");

public:
  DelayedCopy() = delete;

  DelayedCopy(const DelayedCopy &) = delete;
  DelayedCopy &operator=(const DelayedCopy &) = delete;

  DelayedCopy(DelayedCopy &&) = default;
  DelayedCopy &operator=(DelayedCopy &&) = default;

  DelayedCopy(T &&value) : data_(std::move(value)) {}
  DelayedCopy(const T &cref) : data_(Borrowed<T>(&cref)) {}

  const T &ref() const { return boost::apply_visitor(RefVisitor(), data_); }

  friend T take_ownership(DelayedCopy &&cow) {
    return boost::apply_visitor(TakeOwnershipVisitor(), cow.data_);
  }

private:
  struct RefVisitor : public boost::static_visitor<const T &> {
    const T &operator()(Borrowed<T> ptr) const { return *ptr.get(); }
    const T &operator()(const T &ref) const { return ref; }
  };

  struct TakeOwnershipVisitor : public boost::static_visitor<T> {
    T operator()(Borrowed<T> ptr) const { return T(*ptr.get()); }
    T operator()(T &ref) const { return T(std::move(ref)); }
  };
};

struct Bar {
  Noisy data_;

  auto fl() -> DelayedCopy<Noisy> { return Noisy(); }
  auto fm() -> DelayedCopy<Noisy> { return data_; }

  template <typename Fn> void cpsl(Fn fn) { fn(Noisy()); }
  template <typename Fn> void cpsm(Fn fn) { fn(data_); }
};

static void client_observes(const Noisy &) { std::puts(__func__); }
static void client_requires_ownership(Noisy) { std::puts(__func__); }

int main() {
  Bar a;

  std::puts("DelayedCopy:");
  auto afl = a.fl();
  auto afm = a.fm();

  client_observes(afl.ref());
  client_observes(afm.ref());

  client_requires_ownership(take_ownership(a.fl()));
  client_requires_ownership(take_ownership(a.fm()));

  std::puts("\nCPS:");

  a.cpsl(client_observes);
  a.cpsm(client_observes);

  a.cpsl(client_requires_ownership);
  a.cpsm(client_requires_ownership);
}

Output:

DelayedCopy:
Noisy: move ctor
client_observes
client_observes
Noisy: move ctor
Noisy: move ctor
client_requires_ownership
Noisy: copy ctor
client_requires_ownership

CPS:
client_observes
client_observes
client_requires_ownership
Noisy: copy ctor
client_requires_ownership

Are there better techniques to pass values that avoid extra copies yet are still general (allow returning both temporaries and data members)?

On a side note: the code was compiled with g++ 5.2 and clang 3.7 in C++11. In C++14 and C++1z DelayedCopy doesn't compile and I'm not sure whether it's my fault or not.

sawyer
  • 473
  • 4
  • 15
  • returning by value allows for either an implicit move or [RVO](https://en.wikipedia.org/wiki/Return_value_optimization) – NathanOliver Oct 03 '15 at 10:57
  • @NathanOliver That's exactly what I meant when writing "no copy, good", no copy = move or copy elision. – sawyer Oct 03 '15 at 11:05

1 Answers1

1

There are probably thousands of 'correct' ways. I would favour one in which:

  1. the the method that delivers the reference or moved object is explicitly stated so no-one is in any doubt.
  2. as little code to maintain as possible.
  3. all code combination compile and do sensible things.

something like this (contrived) example:

#include <iostream>
#include <string>
#include <boost/optional.hpp>

// an object that produces (for example) strings
struct universal_producer
{
    void produce(std::string s)
    {
        _current = std::move(s);
        // perhaps signal clients that there is something to take here?
    }

    // allows a consumer to see the string but does not relinquish ownership
    const std::string& peek() const {
        // will throw an exception if there is nothing to take
        return _current.value();
    }

    // removes the string from the producer and hands it to the consumer
    std::string take() // not const
    {
        std::string result = std::move(_current.value());
        _current = boost::none;
        return result;
    }

    boost::optional<std::string> _current;

};

using namespace std;

// prints a string by reference
void say_reference(const std::string& s)
{
    cout << s << endl;
}

// prints a string after taking ownership or a copy depending on the call context
void say_copy(std::string s)
{
    cout << s << endl;
}

auto main() -> int
{
    universal_producer producer;
    producer.produce("Hello, World!");

    // print by reference
    say_reference(producer.peek());

    // print a copy but don't take ownership
    say_copy(producer.peek());

    // take ownership and print
    say_copy(producer.take());
    // producer now has no string. next peek or take will cause an exception
    try {
        say_reference(producer.peek());
    }
    catch(const std::exception& e)
    {
        cout << "exception: " << e.what() << endl;
    }
    return 0;
}

expected output:

Hello, World!
Hello, World!
Hello, World!
exception: Attempted to access the value of an uninitialized optional object.
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142