Подтвердить что ты не робот

Избегайте дублирования кода при использовании С++ 11 copy & move

С++ 11 "move" - хорошая функция, но мне было сложно избежать дублирования кода (мы все ненавидим это) при одновременном использовании с "копией". Следующий код - это моя реализация простой круглой очереди (неполной), два метода push() почти одинаковы, кроме одной строки.

Я столкнулся со многими подобными ситуациями вроде этого. Любые идеи, как избежать такого дублирования кода без использования макроса?

=== РЕДАКТИРОВАТЬ ===

В этом конкретном примере дублированный код может быть реорганизован и помещен в отдельную функцию, но иногда такой рефакторинг недоступен или не может быть легко реализован.

#include <cstdlib>
#include <utility>

template<typename T>
class CircularQueue {
public:
    CircularQueue(long size = 32) : size{size} {
        buffer = std::malloc(sizeof(T) * size);
    }

    ~CircularQueue();

    bool full() const {
        return counter.in - counter.out >= size;
    }

    bool empty() const {
        return counter.in == counter.out;
    }

    void push(T&& data) {
        if (full()) {
            throw Invalid{};
        }
        long offset = counter.in % size;
        new (buffer + offset) T{std::forward<T>(data)};
        ++counter.in;
    }

    void push(const T& data) {
        if (full()) {
            throw Invalid{};
        }
        long offset = counter.in % size;
        new (buffer + offset) T{data};
        ++counter.in;
    }

private:
    T* buffer;
    long size;
    struct {
        long in, out;
    } counter;
};
4b9b3361

Ответ 1

Самое простое решение здесь - сделать параметр ссылкой для пересылки. Таким образом, вы можете уйти только с одной функцией:

template <class U>
void push(U&& data) {
    if (full()) {
        throw Invalid{};
    }
    long offset = counter.in % size;
    // please note here we construct a T object (the class template)
    // from an U object (the function template)
    new (buffer + offset) T{std::forward<U>(data)};
    ++counter.in;
}

Есть недостатки в методе:

  • он не является общим, то есть он не всегда может быть выполнен (тривиально). Например, когда параметр не так прост, как T (например, SomeType<T>).

  • Вы задерживаете проверку типа параметра. Длительная и, казалось бы, несвязанная ошибка компилятора может следовать, когда push вызывается с неправильным типом параметра.


Кстати, в вашем примере T&& не является ссылкой на пересылку. Это справочная ссылка. Это потому, что T не является параметром шаблона функции. Это класс, который он уже вывел, когда экземпляр класса создается. Таким образом, правильный способ написать код:

void push(T&& data) {
    ...
    ... T{std::move(data)};
    ...
}

void push(const T& data) {
   ... T{data};
   ...
}

Ответ 2

Решение использования ссылки пересылки является хорошим. В некоторых случаях это становится трудным или раздражающим. В качестве первого шага оберните его интерфейсом, который принимает явные типы, а затем в файле cpp отправите их в реализацию шаблона.

Теперь иногда этот первый шаг также терпит неудачу: если есть N разных аргументов, которые все нужно переслать в контейнер, для этого требуется интерфейс размером 2 ^ N, и, возможно, он должен пересечь несколько уровней интерфейсов, чтобы получить к реализации.

С этой целью вместо переноса или принятия определенных типов мы можем выполнять действие (-ы) конца. В самом внешнем интерфейсе мы преобразуем произвольные типы в эти/те действия (действия).

template<class T>
struct construct {
  T*(*action)(void* state,void* target)=nullptr;
  void* state=nullptr;
  construct()=default;
  construct(T&& t):
    action(
      [](void*src,void*targ)->T*{
        return new(targ) T( std::move(*static_cast<T*>(src)) );
      }
    ),
    state(std::addressof(t))
  {}
  construct(T const& t):
    action(
      [](void*src,void*targ)->T*{
        return new(targ) T( *static_cast<T const*>(src) );
      }
    ),
    state(const_cast<void*>(std::addressof(t)))
  {}
  T*operator()(void* target)&&{
    T* r = action(state,target);
    *this = {};
    return r;
  }
  explicit operator bool()const{return action;}
  construct(construct&&o):
    construct(o)
  {
    action=nullptr;
  }
  construct& operator=(construct&&o){
    *this = o;
    o.action = nullptr;
    return *this;
  }
private:
  construct(construct const&)=default;
  construct& operator=(construct const&)=default;
};

Как только у вас есть объект construct<T> ctor, вы можете создать экземпляр T через std::move(ctor)(location), где местоположение - это правильно указатель указателя, чтобы сохранить T с достаточным объемом памяти.

A constructor<T> может быть неявно преобразован из rvalue или lvalue T. Он также может быть усилен поддержкой emplace, но для этого требуется более качественная комбинация (или больше накладных расходов).

Живой пример. Шаблон является относительно простым стиранием типа. Мы сохраняем операцию в указателе функции и данных в указателе void и восстанавливаем данные из указателя void в сохраненном указателе функции действия.

Существует скромная стоимость описанного выше метода стирания/времени выполнения.

Мы также можем реализовать его следующим образом:

template<class T>
struct construct :
  private std::function< T*(void*) >
{
  using base = std::function< T*(void*) >;
  construct() = default;
  construct(T&& t):base(
    [&](void* target)mutable ->T* {
      return new(target) T(std::move(t));
    }
  ) {}
  construct(T const& t):base(
    [&](void* target)->T* {
      return new(target) T(t);
    }
  ) {}
  T* operator()(void* target)&&{
    T* r = base::operator()(target);
    (base&)(*this)={};
    return r;
  }
  explicit operator bool()const{
    return (bool)static_cast<base const&>(*this);
  }
};

который полагается на std::function, делая стирание типа для нас.

Поскольку это предназначено для работы только один раз (мы переходим от источника), я заставляю контекст rvalue и исключаю свое состояние. Я также скрываю факт, что я std:: function, потому что он не соответствует этим правилам.

Ответ 3

Предисловие

Представление дублирования кода при добавлении поддержки семантики перемещения к вашему интерфейсу очень раздражает. Для каждой функции вы должны сделать две почти идентичные реализации: ту, которая копирует из аргумента, и ту, которая перемещается из аргумента. Если функция имеет два параметра, она даже не выполняет дублирование кода, это код с четырьмя расширениями:

void Func(const TArg1  &arg1, const TArg2  &arg2); // copies from both arguments
void Func(const TArg1  &arg1,       TArg2 &&arg2); // copies from the first, moves from the second
void Func(      TArg1 &&arg1, const TArg2  &arg2); // moves from the first, copies from the second
void Func(      TArg1 &&arg1,       TArg2 &&arg2); // moves from both

В общем случае вам нужно сделать до 2 ^ N перегрузок для функции, где N - количество параметров. По моему мнению, это делает семантику переноса практически непригодной. Это самая разочаровывающая особенность С++ 11.

Проблема могла произойти еще раньше. Давайте рассмотрим следующий фрагмент кода:

void Func1(const T &arg);
T Func2();

int main()
{
    Func1(Func2());
    return 0;
}

Очень странно, что временный объект передается в функцию, которая принимает ссылку. У временного объекта может даже не быть адреса, его можно кэшировать в регистре, например. Но С++ позволяет передавать временные интервалы, где принимается ссылка const (и только const). В этом случае срок жизни временного продлевается до конца срока службы ссылки. Если бы не было этого правила, нам бы пришлось сделать две реализации даже здесь:

void Func1(const T& arg);
void Func1(T arg);

Я не знаю, почему было создано правило, позволяющее передавать временные ссылки, где была принята ссылка (ну, если бы не было этого правила, мы бы не смогли вызвать конструктор копирования, чтобы сделать копию временного объекта, поэтому Func1(Func2()) где Func1 is void Func1(T arg) не будет работать в любом случае:)), но с этим правилом нам не нужно делать две перегрузки функции.

Решение №1: совершенная переадресация

К сожалению, нет такого простого правила, которое бы не требовало реализации двух перегрузок одной и той же функции: той, которая принимает ссылку на константу lvalue и ту, которая принимает ссылку rvalue. Вместо этого была разработана совершенная переадресация

template <typename U>
void Func(U &&param) // despite the fact the parameter has "U&&" type at declaration,
                     // it actually can be just "U&" or even "const U&", it’s due to
                     // the template type deducing rules
{
    value = std::forward<U>(param); // use move or copy semantic depending on the 
                                    // real type of param
}

Это может выглядеть как простое правило, которое позволяет избежать дублирования. Но это не просто, он использует неочевидный шаблон "магия" для решения проблемы, а также это решение имеет некоторые недостатки, вытекающие из того факта, что функция, использующая безупречную переадресацию, должна быть шаблонизирована:

  • Реализация функции должна быть расположена в заголовке.
  • Он раздувает двоичный размер, потому что для каждой используемой комбинации типа параметров (копирования/перемещения) он генерирует отдельную реализацию (у вас есть единая реализация в исходном коде и в то же время у вас есть до 2 ^ N реализаций в двоичный файл).
  • Нет проверки типа для аргумента. Вы можете передать значение любого типа в функцию (поскольку функция принимает тип шаблона). Фактическая проверка будет выполняться в точках, где фактически используется параметр. Это может привести к непонятным сообщениям об ошибках и привести к неожиданным последствиям.

Последняя проблема может быть решена путем создания оболочек без шаблонов для функций идеальной пересылки:

public:
    void push(      T &&data) { push_fwd(data); }
    void push(const T  &data) { push_fwd(data); }

private:
    template <typename U>
    void push_fwd(U &&data)
    {
        // actual implementation
    }

Конечно, его можно использовать на практике, только если функция имеет несколько параметров (один или два). В противном случае вы должны сделать слишком много оберток (до 2 ^ N, вы знаете).

Решение №2: проверка выполнения для подвижности

В конце концов я понял, что проверка аргументов для movablity должна выполняться не во время компиляции, а во время выполнения. Я создал класс reference-wrapper с конструкторами, которые использовали оба типа ссылок (rvalue и const lvalue). Класс сохранил переданную ссылку на конструктор в качестве ссылки на константу lvalue и дополнительно сохранил флаг, была ли переданная ссылка rvalue. Затем вы можете проверить во время выполнения, была ли исходная ссылка rvalue, и если вы просто запустили сохраненную ссылку на rvalue-reference.

Неудивительно, что кто-то еще дошел до этой идеи. Он назвал это "in idiom" (я назвал этот "pmp" - возможно подвижный параметр). Вы можете подробно прочитать об этой идиоме здесь и здесь (оригинальная страница о "в" идиоме, я рекомендую прочитать все 3 части статьи, если вы действительно заинтересованы в проблеме, в статье подробно рассматривается проблема).

Короче говоря, реализация идиомы выглядит так:

template <typename T> 
class in
{
public:
  in (const T& l): v_ (l), rv_ (false) {}
  in (T&& r): v_ (r), rv_ (true) {}

  bool rvalue () const {return rv_;}

  const T& get () const {return v_;}
  T&& rget () const {return std::move (const_cast<T&> (v_));}

private:
  const T& v_; // original reference
  bool rv_;    // whether it is rvalue-reference
};

(Полная реализация также содержит специальный случай, когда некоторые типы могут быть неявно преобразованы в T)

Пример использования:

class A
{
public:
  void set_vec(in<std::vector<int>> param1, in<std::vector<int>> param2)
  {
      if (param1.rvalue()) vec1 = param1.rget(); // move if param1 is rvalue
      else                 vec1 = param1.get();  // just copy otherwise
      if (param2.rvalue()) vec2 = param2.rget(); // move if param2 is rvalue
      else                 vec2 = param2.get();  // just copy otherwise
  }
private:
  std::vector<int> vec1, vec2;
};

В реализации "in" отсутствуют конструкторы копирования и перемещения.

class in
{
  ...
  in(const in  &other): v_(other.v_), rv_(false)     {} // always makes parameter not movable
                                                        // even if the original reference
                                                        // is movable
  in(      in &&other): v_(other.v_), rv_(other.rv_) {} // makes parameter movable if the
                                                        // original reference was is movable
  ...
};

Теперь мы можем использовать его таким образом:

void func1(in<std::vector<int>> param);
void func2(in<std::vector<int>> param);

void func3(in<std::vector<int>> param)
{
    func1(param); // don't move param into func1 even if original reference
                  // is rvalue. func1 will always use copy of param, since we
                  // still need param in this function

    // some usage of param

    // now we don’t need param
    func2(std::move(param)); // move param into func2 if original reference
                             // is rvalue, or copy param into func2 if original
                             // reference is const lvalue
}

Мы могли бы также перегрузить оператор присваивания:

template<typename T>
T& operator=(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
    return lhs;
}

После этого нам не нужно было каждый раз проверять ravlue, мы могли бы просто использовать его таким образом:

   vec1 = std::move(param1); // moves or copies depending on whether param1 is movable
   vec2 = std::move(param2); // moves or copies depending on whether param2 is movable

Но, к сожалению, С++ не допускает перегрузку operator= в качестве глобальной функции (fooobar.com/info/253398/...). Но мы можем переименовать эту функцию в assign:

template<typename T>
void assign(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
}

и используйте его следующим образом:

    assign(vec1, std::move(param1)); // moves or copies depending on whether param1 is movable
    assign(vec2, std::move(param2)); // moves or copies depending on whether param2 is movable

Также это не будет работать с конструкторами. Мы не можем просто написать:

std::vector<int> vec(std::move(param));

Для поддержки этой функции требуется стандартная библиотека:

class vector
{
    ...
public:
    vector(std::in<vector> other); // copy and move constructor
    ...
}

Но стандарты ничего не знают о нашем классе "в". И здесь мы не можем сделать обходной путь, похожий на assign, поэтому использование класса "in" ограничено.

Послесловие

T, const T&, T&& для параметров слишком много для меня. Прекратите вводить то, что делает то же самое (ну, почти то же самое). T достаточно!

Я бы предпочел написать так:

// The function in ++++C language:
func(std::vector<int> param) // no need to specify const & or &&, param is just parameter.
                             // it is always reference for complex types (or for types with
                             // special qualifier that says that arguments of this type
                             // must be always passed by reference).
{
    another_vec = std::move(param); // move parameter if it movable.
                                    // compiler hides actual rvalue-ness
                                    // of the arguments in its ABI
}

Я не знаю, считал ли стандартный комитет такой реализацией семантики перемещения, но, возможно, слишком поздно, чтобы сделать такие изменения на С++, потому что они сделают ABI компиляторов несовместимыми с предыдущими версиями. Также он добавляет некоторые служебные данные во время выполнения, и могут быть другие проблемы, которые мы не знаем.