LINUX.ORG.RU

Шаблонный класс с поддержкой и копирования, и перемещения (rvalue) + тип без конструктора копирования

 ,


0

1

Всем привет.

Немного перефразирую тему. Представим, что мы пишем аналог std::vector и его функции push_back. Фишка в том, что эта функция бывает в двух вариантах: копирование и перемещение; из документации:

void push_back (const value_type& val);
void push_back (value_type&& val);
А теперь у нас есть тип без конструктора копирования, но с конструктором перемещения. Проблема в том, что в таком варианте код не компилируется, так как вариант функции с копированием будет некорректным. Чтобы было понятней, у меня такое:
template<class TValue, typename TIndex>
TIndex CSet<TValue,TIndex>::Add(const TValue &value)
{
	TIndex result;
	TValue copyValue(value);
	
	result=Add( std::move(copyValue) ); // Функция добавления с перемещением

	return result;
}

------

std2.cpp: In instantiation of ‘TIndex CSet<TValue, TIndex>::Add(const TValue&) [with TValue = CElement; TIndex = TElementKey]’:
std2.cpp:1366:2:   required from here
std2.cpp:713:24: error: use of deleted function ‘CElement::CElement(const CElement&)’
  TValue copyValue(value);
                        ^
std2.cpp:556:2: error: declared here
  CElement(const CElement &copyValue)=delete;
  ^

Как сделать шаблонный класс с поддержкой и копирования, и перемещения (rvalue), и чтобы это работало с типом без конструктора копирования? Ведь в std::vector это как-то делают?

P. S. Исходник std::vector::push_back мне взрывает моск строкой с _Alloc_traits . Думаю, ответ именно в этой строке, но никак не могу его ухватить. Поможете?

      void
      push_back(const value_type& __x)
      {
        if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
          {
            _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                                     __x);
            ++this->_M_impl._M_finish;
          }
        else
#if __cplusplus >= 201103L
          _M_emplace_back_aux(__x);
#else
          _M_insert_aux(end(), __x);
#endif
      }

#if __cplusplus >= 201103L
      void
      push_back(value_type&& __x)
      { emplace_back(std::move(__x)); }

★★★★★

Последнее исправление: Kroz (всего исправлений: 1)

Зачем ты принимаешь объект по константной ссылке, а потом сразу его копируешь? Почему не принимать по значению?

devsdc ★★
()
Ответ на: комментарий от devsdc

Зачем ты принимаешь объект по константной ссылке, а потом сразу его копируешь? Почему не принимать по значению?

Чтобы не через стек. Потенциально он может быть достаточно большим.

Kroz ★★★★★
() автор топика

А покажи, как вызывается Add. Мне казалось, что неиспользуемые функции (при определённых условиях) не должны инстанцироваться.

devsdc ★★
()
Ответ на: комментарий от devsdc

Add() вызывается нормально:

key=m_elements.Add( std::move(el) );

Я тебе больше скажу: если я удаляю вариант с копированием, то все работает. Но как тогда быть в ситуациях, когда правда нужно копирование/дубликация?

И я бы плюнул, сделал бы у элементов пустой коструктор копирования с одной только строкой вида throw «Так делать нельзя» дабы уберечься от собственной невнимательности (и я так поначалу делал). Но проблема в том, что vector (да, он там внутри) при добавлении уже второго элемента делает реаллокацию и переносит элементы именно с помощью конструктора копирования. Если я, методом описанным параграфом выше, делаю вариант с чистым перемещением, то, как я сказал, все работает. То есть перемещение с помощью конструктора перемещения (сорри за тавтологию) в векторе таки возможно. Вывод: мне правда нужен тип совсем без конструктора копирования, даже пустого. И мне нужен шаблонный контейнер с поддержкой и копирования, и перемещения.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от devsdc

Мне казалось, что неиспользуемые функции (при определённых условиях) не должны инстанцироваться.

AFAIK, они должны в любом случае скомпилиться. Но при линковке их код может не включиться в результирующий файл. Такая себе оптимизация по размеру. AFAIK.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Stil

А с type traits это не делается?

Как?

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Stil

Общая реализация и с тем, и с другим, специализация без копирования

У меня класс немаленький, плюс в шаблоне 2 типа: нужно будет специализацию делать по обоим, ибо по одному нельзя.

Пока написал такой КОСТЫЛИЩЕ.

template<bool icc>
class CMoveCopyDetector{
public:
	template<class T>
	inline static T MoveCopy(T &v)
	{
		T result(v);

		return result;
	};
	
};
template<>
class CMoveCopyDetector<false>{
public:
	template<class T>
	inline static T&& MoveCopy(T &&v)
	{
		THROW("ELogicError: You should not get here, since the value type has no copy constructor.");
		throw ELogicError();
		//return std::move(v);
	};
	
};


template<class TValue, typename TIndex>
TIndex CSet<TValue,TIndex>::Add(const TValue &value)
{
	FUNCTION("&value");
	
	TIndex result;
	
	result=Add( std::move(  CMoveCopyDetector< std::is_copy_constructible<TValue>::value >::MoveCopy(value)   ) );
	
	RETURN(result);
	return result;
}

Работает. Правда, если убрать исключение и раскомментровать return, то при неправильном использовании TValue (без std::move, несмотря на отсутствие конструктора копирования) входит в бесконечную рекурсию (Add() вызывает сама себя), не совсем понятно почему.

Должен быть вариант получше...

Kroz ★★★★★
() автор топика
Последнее исправление: Kroz (всего исправлений: 1)

Ты как-то неправильно вызвал свою Add снаружи, раз выбралась функция, принимающая lvalue, а не rvalue ссылку. И нам не показал эту часть кода.

Общую рекомендацию передавать по значению выше уже сказали. И не надо говорить про большие объекты на стеке, ты же не боишься copyValue на стек класть. Другой вариант — perfect forwarding.

const86 ★★★★★
()

Ты перемудрил. Проблема в том, что у тебя инстанциируется функция Add(const TValue &value), а не Add(TValue && value). То есть, в вызывающем коде ты передаёшь в Add lvalue, а не rvalue. Покажи код вызывающий Add.

rupert ★★★★★
()

что говорят реализации STL ?

qulinxao ★★☆
()

А разве можно вообще перемещать объект который пришел по КОНСТАНТНОЙ ссылке?

anonymous
()
Ответ на: комментарий от anonymous

аа тыж скопировать пытаешься сначала...так сделай что-то типа emplace у вектора.

anonymous
()

Что-то я не понял, что у тебя не работает. Можешь сделать минимальный неработающий пример?

http://ideone.com/ugs4U9

#include <iostream>

template <typename T>
struct vec {
	void add(const T& a) {
		T t(a);
	}

	void add(T&& a) {
		
	}
};

struct mov {
	mov() = default;
	mov(const mov&) = delete;
	mov(mov&&) = default;
};

int main() {
	mov m;
	
	vec<mov> v;
	
	v.add(std::move(m));

	return 0;
}

Из твоего сообщения компилятора, создается впечатление, что ты вызвал CSet<TValue,TIndex>::Add(const TValue &value) для объекта без конструктора копирования (stl vector так тоже не умеет, естественно).

Kuzy ★★★
()
Ответ на: комментарий от Begemoth

Потому что это быдлокод, который потенциально ведёт к двойному вызову деструктора. Попробуй экспешн рандомно кидать из конструктора.

nerdogeek
()
Ответ на: комментарий от nerdogeek

Это не быдлокод. ТС пишет класс-последовательность, и без явного вызова деструктора, он не реализует тот-же push_back (при перемещении элементов в новую область памяти). Это добавляет требование nothrow для деструктора и конструктора перемещения.

Begemoth ★★★★★
()
Ответ на: комментарий от Begemoth

Очевидно же, убивать вместе с авторами стандартной библиотеки.

А заодно и геймдевелоперов, которые используют свой аллокатор и placement new ;)

andreyu ★★★★★
()
Ответ на: комментарий от nerdogeek

В том коде, что привёл ТС - да, но в общем случае контейнера явный вызов деструктора и placement new - необходимы. Память под объект не обязана быть выделена оператором new, а тогда оператор присваивания тебе ничем не поможет, вызов нестатических функций членов с this, указывающм на сырую память (в которой не был создан объект) - UB.

Begemoth ★★★★★
()
Ответ на: комментарий от andreyu

У ТС не параметризуется аллокатором его контейнер. В общем, решение уже подсказали выше по треду.

nerdogeek
()

Как сделать шаблонный класс с поддержкой и копирования, и перемещения (rvalue), и чтобы это работало с типом без конструктора копирования?

std::enable_if, не?

slovazap ★★★★★
()
Ответ на: комментарий от Kuzy

А теперь сделай функции виртуальными:

	virtual void add(const T& a) {
		T t(a);
	}

	virtual void add(T&& a) {
		
	}
И на всякий случай дай версию своего компилятора. У меня:
$ gcc -v
Using built-in specs.
COLLECT_GCC=/usr/i686-pc-linux-gnu/gcc-bin/4.8.3/gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/i686-pc-linux-gnu/4.8.3/lto-wrapper
Target: i686-pc-linux-gnu
Configured with: /var/tmp/portage/sys-devel/gcc-4.8.3/work/gcc-4.8.3/configure --host=i686-pc-linux-gnu --build=i686-pc-linux-gnu --prefix=/usr --bindir=/usr/i686-pc-linux-gnu/gcc-bin/4.8.3 --includedir=/usr/lib/gcc/i686-pc-linux-gnu/4.8.3/include --datadir=/usr/share/gcc-data/i686-pc-linux-gnu/4.8.3 --mandir=/usr/share/gcc-data/i686-pc-linux-gnu/4.8.3/man --infodir=/usr/share/gcc-data/i686-pc-linux-gnu/4.8.3/info --with-gxx-include-dir=/usr/lib/gcc/i686-pc-linux-gnu/4.8.3/include/g++-v4 --with-python-dir=/share/gcc-data/i686-pc-linux-gnu/4.8.3/python --enable-objc-gc --enable-languages=c,c++,objc,obj-c++,fortran --enable-obsolete --enable-secureplt --disable-werror --with-system-zlib --enable-nls --without-included-gettext --enable-checking=release --with-bugurl=https://bugs.gentoo.org/ --with-pkgversion='Gentoo 4.8.3 p1.1, pie-0.5.9' --enable-libstdcxx-time --enable-shared --enable-threads=posix --enable-__cxa_atexit --enable-clocale=gnu --disable-multilib --disable-altivec --disable-fixed-point --with-arch=i686 --enable-targets=all --disable-libgcj --enable-libgomp --disable-libmudflap --disable-libssp --enable-lto --without-cloog
Thread model: posix
gcc version 4.8.3 (Gentoo 4.8.3 p1.1, pie-0.5.9) 

Kroz ★★★★★
() автор топика
Ответ на: комментарий от const86

И не надо говорить про большие объекты на стеке, ты же не боишься copyValue на стек класть.

Где?

Kroz ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А разве можно вообще перемещать объект который пришел по КОНСТАНТНОЙ ссылке?

Я тоже был удивлен. После экспериментов выяснилось, что оно работает просто как ссылка не вызывая конструктор перемещения. Баг?

Kroz ★★★★★
() автор топика

devsdc, Stil, const86, rupert, qulinxao, nerdogeek, Begemoth, andreyu, slovazap
Кастую откликнувшихся, ибо есть сподвижки. Пример товарища Kuzy компилится. Но, если сделать обе функции Add виртуальными - не компилится с такой же ошибкой. Соответственно:
1. WTF это нормально? Почему так?
2. Если у кого компилится с виртуальными функциями - дайте версию компилятора.

Спасибо.

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

std::move() не вызывает конструктор перемещения, если что.

Олсо, перемещается-то не объект, пришедший по константной ссылке, а его копия.

devsdc ★★
()
Ответ на: комментарий от devsdc

std::move() не вызывает конструктор перемещения

Точнее, не всегда вызывает.

devsdc ★★
()
Ответ на: комментарий от Kroz

Для виртуальных функций код всегда генерируется в классе-шаблоне, чтобы поместить их в виртуальную таблицу. Сравни это в невиртуальными функциями, где код будет сгенерирован, только если ты позовёшь функцию. Поэтому опять-же, ты что-то делаешь не так. Не делай эти функции виртуальными.

rupert ★★★★★
()
Ответ на: комментарий от rupert

Для виртуальных функций код всегда генерируется в классе-шаблоне, чтобы поместить их в виртуальную таблицу. Сравни это в невиртуальными функциями, где код будет сгенерирован, только если ты позовёшь функцию.

Где это описано? Это компилятор или стандарт?

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

Где это описано? Это компилятор или стандарт?

Описано в стандарте, что компилятор может это делать (а может и не делать). Вообщем это не баг.

14.7.1.10

An implementation shall not implicitly instantiate a function template, a member template, a non-virtual member function, a member class, or a static data member of a class template that does not require instan- tiation. It is unspecified whether or not an implementation implicitly instantiates a virtual member function of a class template if the virtual member function would not otherwise be instantiated. The use of a template specialization in a default argument shall not cause the template to be implicitly instantiated except that a class template may be instantiated where its complete type is needed to determine the correctness of the default argument. The use of a default argument in a function call causes specializations in the default argument to be implicitly instantiated.

Kuzy ★★★
()
Ответ на: комментарий от Kuzy

Ясно, спасибо. Буду думать как мне поступать: либо функцию сделать обычной, либо enable_if делать...

Kroz ★★★★★
() автор топика
Ответ на: комментарий от Kroz

enable_if

Он будет всегда срабатывать, даже если функцию нигде не вызвать.

Я ничего лучше чем это, не придумал.

#include <iostream>
#include <type_traits>

template <typename T>
struct vec;

template <typename T, bool B>
struct vec_impl {
	static void add(const vec<T>& self, const T& a) {
		// выполнение вот этого - всегда ошибка, но ловится только в рантайме.
	}
};

template <typename T>
struct vec_impl<T, true> {
	static void add(const vec<T>& self, const T& a) {
		// vec::add, self = this
		T n(a);
	}
};

template <typename T>
class vec {
	friend struct vec_impl<T, true>;
	
public:
	virtual void add(const T& a) {
		vec_impl<T, std::is_copy_constructible<T>::value>::add(*this, a);
	}

	virtual void add(T&& a) {
		
	}
};

struct mov {
	mov() = default;
	mov(const mov&) = delete;
	mov(mov&&) = default;
};

int main() {
	mov m;
	
	vec<mov> v;
	
	v.add(std::move(m));
	v.add(m); // статически не проверит, скомпилируется

	return 0;
}
Kuzy ★★★
()
Ответ на: комментарий от Kuzy

выполнение вот этого - всегда ошибка, но ловится только в рантайме.

Зачем тогда не template <typename T, bool B> struct vec_impl; ? Иначе у тебя и для получается сначала для false c true в первом vec_impl, потом ты специализируешь для true, хотя тебе нужен только он и отсутствующий для false.

anonymous
()
Ответ на: комментарий от anonymous

Прикол в том, что компилятор, при обращении к vec<T>, неявно инстанциирует vec<T>::add(const T& a) который в свою очередь тянет за собой проверки типов всего, что содержится в vec<T>::add(const T& a), даже если для конкретного T, vec<T>::add(const T& a) не должен работать.

Вот, vec<T>::add(const T& a) ни разу не вызван, но проверку типов не проходит: http://ideone.com/pZWRKg

Kuzy ★★★
()
Последнее исправление: Kuzy (всего исправлений: 2)
Ответ на: комментарий от Kuzy
template <typename T, typename Enable = void> struct vec;
template <typename T> struct vec<T, typename std::enable_if<std::is_copy_constructible<T>::value>::type> { virtual void add(T const&) { puts("copy"); } };
template <typename T> struct vec<T, typename std::enable_if<!std::is_copy_constructible<T>::value>::type> { virtual void add(T&&) { puts("move"); } };
anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.