Сама по себе идея хороша, просто данная задача слишком проста для неё, не стоит усложнять без веской на то причины. С другой стороны, тогда и писать было бы не о чем ;-)
На мой взгляд, в данном случае такое решение — overkill, проще было бы руками вписать ничего не делающие clear и buffer в варианты initializator, где этих функций нету. Если уж совсем не хочется дублировать код, то сделать для initializator базовый класс, который реализует пустые функции (не надо никакой виртуальности), а в тех initializator, где нужно, просто переопределить их.
Ещё, на мой взгляд, вместо такого:
template<class LogType>
class initializator{};
лучше написать код, который выдает ошибку, если такой шаблон инстанциируется. Потому что вкупе с вашим решением для clear и buffer код типа Logger<int>::buffer() прекрасно компилируется. А может лучше вообще убрать такой шаблон, который используется только для своей полной специализации, и написать 3 отдельных класса?
Ну и напоследок, у вас в классе Logger удален оператор копирования, но не удален конструктор копирования.
Не знаю как преподавать в младших классах, но если брать среднюю школу, то тут все далеко не так просто. Легко рассказывать интересные вещи заинтересованным в них людях. А на практике список вещей, которые вы должны рассказать, задан вам сверху программой курса, а большинство учеников сидят на уроке с мыслью «побыстрее бы перемена» или «побыстрее бы домой». А ещё вам может «повезти» и у вас будут дети, которые глубоко разбираются в вашем предмете. В кавычках, потому что для них вы рассказываете банальные и очевидные вещи, и им, разумеется, скучно. И, например, если вы зададите классу вопрос на понимание материала, то такой ребенок через 5 секунд вам скажет ответ, а остальные ещё сам вопрос до конца осознать не успеют. В общем, вы вынуждены лавировать между несколькими сторонами, и скорей всего это приведет к тому, что ни одна из них не будет вами довольна.
Там, кстати, баг в коде: в вариантах construct, которые принимают rvalue ссылку в качестве аргумента, надо её смувить или сфорвардить перед передачей дальше, в общем, из lvalue преобразовать в xvalue.
К слову, насчет первой хотелки, в теории её нетрудно реализовать, надо просто на нижнем уровне, где конструируется объект, заменить в его инициализации круглые скобочки фигурными. Тогда, если наша структура выглядит как-то так:
struct A {
int x;
double y;
const char* z;
};
то после вызова emplace_back(10, 20.0, "abcd"), она будет проинициализирована списком {10, 20.0, "abcd"}. Конечно, в таком подходе есть свои недостатки: для произвольных типов такая инициализация может скрывать конструкторы (скажем, vector<int>(10, 20) — это не то же самое, что vector<int>{10, 20}), для аггрегатов у нас отвалится инициализация копированием (нельзя будет вызвать emplace_back(A{}), например), кроме того мы все равно никак не сможем передать вложенные списки (типа { {1, 2, 3}, {}, {4, 5} }. В целом, если делать так только для аггрегатных типов и починить им инициализацию копированием, перегрузив для неё отдельно emplace_back, то первые две проблемы разрешимы.
Более того, в теории мы уже сейчас можем заставить стандартные контейнеры себя так вести (правда функцию проверки is_aggregate нам не завезли), стандарт говорит нам про них:
For the components affected by this subclause that declare an allocator_type, objects stored in these components shall be constructed using the allocator_traits<allocator_type>::construct function and destroyed using the allocator_traits<allocator_type>::destroy function (20.6.8.2). These functions are called only for the container’s element type, not for internal types used by the container. [ Note: This means, for example, that a node-based container might need to construct nodes containing aligned buffers and call construct to place the element into the buffer. —end note]
а allocator_traits<allocator_type>::construct в свою очередь вызывает метод construct переданного ей аллокатора, если этот метод определен. То есть нам всего лишь нужно создать свой аллокатор с методом construct, который будет делать инициализацию нужным нам способом. На практике все сложнее, путем небольшим плясок с бубном мне удалось завести это для вектора:
Код
#include <memory>
#include <vector>
#include <string>
#include <iostream>
#include <cmath>
template<typename T>
struct init_list_allocator : public std::allocator<T> {
template<typename... Args>
void construct(T* p, Args&&... args)
{ ::new((void *)p) T{std::forward<Args>(args)...}; }
// Чиним инициализацию копированием
void construct(T* p, T& copy_construct_arg)
{ std::allocator<T>::construct(p, copy_construct_arg); }
void construct(T* p, const T& copy_construct_arg)
{ std::allocator<T>::construct(p, copy_construct_arg); }
void construct(T* p, const T&& copy_construct_arg)
{ std::allocator<T>::construct(p, copy_construct_arg); }
void construct(T *p, T&& move_construct_arg)
{ std::allocator<T>::construct(p, move_construct_arg); }
// Без этого не работает, потому что вместо переданного типа аллокатора
// реализация зачем-то использует Allocator::rebind<T>::other
template<typename U>
struct rebind {
using other = init_list_allocator<U>;
};
};
template<class T>
using improved_vector = std::vector<T, init_list_allocator<T>>;
struct A {
int x;
double y;
const char* z;
};
int main()
{
using namespace std;
vector<string> strings;
improved_vector<A> v;
for (int i = 0; i < 21; ++i) {
strings.emplace_back(to_string(i*i));
v.emplace_back(i, sqrt(i), strings.back().c_str());
};
for (const auto& elem : v)
cout << elem.x << ' ' << elem.y << ' ' << elem.z << '\n';
}
а вот для std::list уже ничего не выходит, потому что в глубинах его реализации объект инициализируется в конструкторе элемента списка (той штуки, которая помимо объекта содержит ещё указатели на соседей), и там в списке инициализации конструктора стоят круглые скобочки. Причем таким образом происходит и в gcc, и в clang. Почему они так поступают, когда стандарт вроде как говорит иное, я не знаю.
Если поменять int на что-нибудь посложнее, то компилироваться перестает. В clang и GCC 4.9 начинает работать, но вообще это видимо баг стандарта, потому что про list-initialization там написано следующее:
List-initialization of an object or reference of type T is defined as follows:
If the initializer list has no elements and T is a class type with a default constructor...
Otherwise, if T is an aggregate...
Otherwise, if T is a specialization of std::initializer_list<E>...
Otherwise, if T is a class type...
Otherwise, if T is a reference type, a prvalue temporary of the type referenced by T is list-initialized, and the reference is bound to that temporary. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. —end note ]
Otherwise, if the initializer list has a single element, the object or reference is initialized from that element;...
...
Тут явно какая-то ошибка, потому что до нижнего пункта в случае ссылок мы никогда не дойдем, а предпоследний пункт говорит нам выдавать compilation error на неконстантные lvalue ссылки, а все остальные инициализировать путем обязательного создания временной переменной и привязки её к ссылке, что тоже весьма странно.
В С++14 это дело подправили и теперь оно звучит так:
...
Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element; if
a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.
Otherwise, if T is a reference type, a prvalue temporary of the type referenced by T is copy-list-initialized or direct-list-initialized, depending on the kind of initialization for the reference, and the reference is bound to that temporary. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. —end note ]
Нетрудно увидеть, что при описанном в статье способе использования нам нужна только первая перегрузка. Поэтому интересно, а для чего нужен второй вариант, да ещё и с такой проверкой (которая также требуется по стандарту)? Видимо есть какие-то другие разумные способы использования std::forward, для которых она нужна?
На stackoverflow есть такой вопрос, там отвечают, что это уберегает от ошибок. Но при данном способе использования единственные 2 ошибки, которые можно допустить на мой взгляд — это забыть написать после forward тип в угловых скобочках, и тогда компилятор и при одной версии ругается, что не может его вывести, или забыть написать "&&" в списке аргументов функции-обертки (или написать один амперсанд вместо двух), но тогда компилятор нас за руку не ловит и никаких ошибок не выдает.
Это неправда, шанс не получить коллизию для n ключей, если всевозможных вариантов ключей — k, равен:
(1 – 1/k)(1 – 2/k)...(1 – n/k)
а не как у вас:
(1 – 1/k)n
Если взять в качестве примера известный "парадокс дней рождения", то для n = 23 и k = 365, по вашей формуле имеем (364/365)23 ≈ 94%, а на самом деле там около 50%.
Если хотите, то можно оценить это произведение снизу как (1 – n/k)n
Согласен, но аддитивную стратегию сей факт лучше не делает (о чем собственно и статья). Даже если одно перемещение недорогое, нам придется делать их много, но даже если нам сказочно повезет и ничего никогда перемещать не придется, то все равно имхо мы должны сильно просесть за счет частоты вызова аллокатора.
Имхо при аддитивной стратегии (каждый раз увеличивать размер на некоторую фиксированную константу) вы на одних вызовах аллокатора просядете, даже если копирование у вас будет дешевым или вообще никогда не будет происходить. Например, если вы будете увеличивать размер со 128 кб до 256 блоками по 64 байта (допустим у нас массив int32 и мы добавляем по 16 элементов каждый раз), то вместо одного-двух вызовов аллокатора вы сделаете 128 * 1024 / 64 = 2048 вызовов, не говоря уж о том, когда счет пойдет на десятки-сотни мегабайт. Так что без какого-либо масштабирования константы такая стратегия в общем случае имхо обречена.
Это цитата из самого первого комментария этой ветки. Всю нашу дискуссию я пытаюсь вам объяснить, в каких условиях рассуждения, приведенные в этом комментарии, имеют смысл (иными словами, какую ситуацию мы моделируем).
Я об этом выше писал, все эти рассуждения (вида: пусть никто не больше не пользуется аллокатором памяти, тогда ...) имеют смысл только если мы делаем не realloc, а malloc нового блока, а затем free старого.
Не очень понял, что вы хотели сказать. Смотрите, пусть мы действуем вышеописанным способом: делаем malloc для буфера нового размера, потом free для старого. Кроме того, больше никто аллокатором не пользуется. Тогда при malloc'e аллокатор каждый раз будет выдавать нам кусок памяти, непосредственно следующий за текущим куском, то есть наша память будет выглядеть следующим образом:
| ранее использовавшаяся память | старый буфер | новый буфер |
И так будет продолжаться до тех пор, пока кусок ранее использовавшейся памяти не станет достаточно велик, чтобы вместить буфер нового размера. Математически можно показать, что для того, чтобы это хоть когда-нибудь произошло, требуется, чтобы наша константа была меньше золотого сечения.
Мне кажется, тут речь была не о простом realloc'е, а о ситуации, когда мы сначала malloc'ом выделяем буфер нового размера, потом каким-то нетривиальный образом перемещаем в него данные, а затем вызываем для старого участка free (так, например, делает vector в С++ для нетривиально копируемых типов, так как он обязан вызвать для каждого элемента конструктор перемещения). Потому что в случае realloc'a описанной ситуации:
Т.е. эти 4 МБ и 8 МБ, скорее всего, расположены друг за другом.
быть не может, на предыдущем шаге наш участок памяти просто расширился бы с 4 мб до 8ми.
Другой, гораздо более типичный способ — представлять в виде функции, вычисляющей новое состояние (это может быть как единая функция для всех состояний, так и по одной штуке для каждого состояния). Пример есть в статье под катом «Наиболее интуитивный, но громоздкий код для подобной задачи». В случае, когда у вас алфавит достаточно велик (а не 4 символа, как у автора), способы, честно хранящие новое состояние для каждой пары <исходное состояние, символ>, дают слишком большой memory overhead.
В теории можно воспользоваться внутри функций-членов Logger.
Ещё, на мой взгляд, вместо такого:
лучше написать код, который выдает ошибку, если такой шаблон инстанциируется. Потому что вкупе с вашим решением для clear и buffer код типа Logger<int>::buffer() прекрасно компилируется. А может лучше вообще убрать такой шаблон, который используется только для своей полной специализации, и написать 3 отдельных класса?
Ну и напоследок, у вас в классе Logger удален оператор копирования, но не удален конструктор копирования.
то после вызова emplace_back(10, 20.0, "abcd"), она будет проинициализирована списком {10, 20.0, "abcd"}. Конечно, в таком подходе есть свои недостатки: для произвольных типов такая инициализация может скрывать конструкторы (скажем, vector<int>(10, 20) — это не то же самое, что vector<int>{10, 20}), для аггрегатов у нас отвалится инициализация копированием (нельзя будет вызвать emplace_back(A{}), например), кроме того мы все равно никак не сможем передать вложенные списки (типа { {1, 2, 3}, {}, {4, 5} }. В целом, если делать так только для аггрегатных типов и починить им инициализацию копированием, перегрузив для неё отдельно emplace_back, то первые две проблемы разрешимы.
Более того, в теории мы уже сейчас можем заставить стандартные контейнеры себя так вести (правда функцию проверки is_aggregate нам не завезли), стандарт говорит нам про них:
а allocator_traits<allocator_type>::construct в свою очередь вызывает метод construct переданного ей аллокатора, если этот метод определен. То есть нам всего лишь нужно создать свой аллокатор с методом construct, который будет делать инициализацию нужным нам способом. На практике все сложнее, путем небольшим плясок с бубном мне удалось завести это для вектора:
то в качестве аргумента в forward всегда будет подаваться lvalue и, соотвественно, выбираться первая перегрузка.
Тут явно какая-то ошибка, потому что до нижнего пункта в случае ссылок мы никогда не дойдем, а предпоследний пункт говорит нам выдавать compilation error на неконстантные lvalue ссылки, а все остальные инициализировать путем обязательного создания временной переменной и привязки её к ссылке, что тоже весьма странно.
В С++14 это дело подправили и теперь оно звучит так:
Нетрудно увидеть, что при описанном в статье способе использования нам нужна только первая перегрузка. Поэтому интересно, а для чего нужен второй вариант, да ещё и с такой проверкой (которая также требуется по стандарту)? Видимо есть какие-то другие разумные способы использования std::forward, для которых она нужна?
На stackoverflow есть такой вопрос, там отвечают, что это уберегает от ошибок. Но при данном способе использования единственные 2 ошибки, которые можно допустить на мой взгляд — это забыть написать после forward тип в угловых скобочках, и тогда компилятор и при одной версии ругается, что не может его вывести, или забыть написать "&&" в списке аргументов функции-обертки (или написать один амперсанд вместо двух), но тогда компилятор нас за руку не ловит и никаких ошибок не выдает.
Можно, это был баг какой-то версии gcc.
(1 – 1/k)(1 – 2/k)...(1 – n/k)
а не как у вас:
(1 – 1/k)n
Если взять в качестве примера известный "парадокс дней рождения", то для n = 23 и k = 365, по вашей формуле имеем (364/365)23 ≈ 94%, а на самом деле там около 50%.
Если хотите, то можно оценить это произведение снизу как (1 – n/k)n
| ранее использовавшаяся память | старый буфер | новый буфер |
И так будет продолжаться до тех пор, пока кусок ранее использовавшейся памяти не станет достаточно велик, чтобы вместить буфер нового размера. Математически можно показать, что для того, чтобы это хоть когда-нибудь произошло, требуется, чтобы наша константа была меньше золотого сечения.