Кои са добрите начини да се избегне копирането, ако извикващият метод не се нуждае от собственост върху данните?

Ето го проблемът, за който си мислех напоследък. Да кажем, че нашият интерфейс е член-функция, която връща обект, който е скъп за копиране и евтин за преместване (std::string, std::vector и т.н.). Някои реализации могат да изчислят резултата и да върнат временен обект, докато други могат просто да върнат обект член.

Примерен код за илюстрация:

// 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;
    }
};

Има и разни "клиенти". Някои само четат данните, други изискват собственост.

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

Кодът по-горе композира добре, но не е толкова ефективен, колкото би могъл да бъде. Ето всички комбинации:

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

Кой е добър начин за коригиране на тази ситуация?

Измислих два начина, но съм сигурен, че има по-добро решение.

При първия си опит написах обвивка на DelayedCopy, която съдържа или стойност T, или указател към const T. Много е грозна, изисква допълнителни усилия, въвежда излишни ходове, пречи на избягването на копиране и вероятно има много други проблеми.

Втората ми мисъл беше стил на продължаващо предаване, който работи доста добре, но превръща функциите на членовете в шаблони за членски функции. Знам, че има функция std::function, но тя има своите допълнителни разходи, така че по отношение на производителността може да е неприемлива.

Примерен код:

#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);
}

Изход:

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

Има ли по-добри техники за предаване на стойности, които избягват допълнителни копия, но все още са общи (позволяват връщане на временни елементи и данни)?

Към странична бележка: кодът е компилиран с g++ 5.2 и clang 3.7 в C++11. В C++14 и C++1z DelayedCopy не се компилира и не съм сигурен дали вината е моя или не.


person sawyer    schedule 03.10.2015    source източник
comment
връщането по стойност позволява или имплицитно преместване, или RVO   -  person NathanOliver    schedule 03.10.2015
comment
@NathanOliver Точно това имах предвид, когато написах без копие, добре, без копие = преместване или копиране elision.   -  person sawyer    schedule 03.10.2015


Отговори (1)


Вероятно има хиляди "правилни" начини. Бих предпочел такъв, в който:

  1. методът, който доставя препратката или преместения обект, е изрично посочен, така че никой не се съмнява.
  2. възможно най-малко код за поддръжка.
  3. всички кодови комбинации компилират и правят разумни неща.

нещо като този (измислен) пример:

#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;
}

очакван резултат:

Hello, World!
Hello, World!
Hello, World!
exception: Attempted to access the value of an uninitialized optional object.
person Richard Hodges    schedule 03.10.2015