2015-10-03 4 views
0

Вот проблема, о которой я думал в последнее время. Скажем, наш интерфейс - это функция-член, которая возвращает объект, который стоит скопировать и дешево перемещать (std :: string, std :: vector, et cetera). Некоторые реализации могут вычислять результат и возвращать временный объект, в то время как другие могут просто возвращать объект-член.Каковы хорошие способы избежать копирования, если вызывающему абоненту метода не требуется владение данными?

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

// 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. Это очень уродливо, требует дополнительных усилий, вводит избыточные ходы, мешает копированию и, вероятно, имеет многие другие проблемы.

Моя вторая мысль была continuation-passing style, которая работает довольно хорошо, но превращает функции-члены в шаблоны функций-членов. Я знаю, что есть 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 не компилируется, и я не уверен, является ли это моей ошибкой или нет.

+0

возвращение по значению позволяет либо неявной двигаться или [РВО] (https: // эн. википедия.org/wiki/Return_value_optimization) – NathanOliver

+0

@NathanOliver Это именно то, что я имел в виду при написании «no copy, good», no copy = move или copy elision. – sawyer

ответ

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.