Комментарии 25
А есть какие-либо принципиальные идейные отличия между LINQ и Ranges? Если нет, то не логичнее ли (если уж выбрали путь идеоматичности) использовать Ranges, тем более они претендуют на включение в стандарт?
В range-v3 поддерживается любой источник удовлетворяющий довольно либеральным требованиям — требуется только возможноть итерации с помощью begin()
и end()
(причём они не обязаны возвращать один и тот же тип, что делает удобным, например, остановку по условию или количеству). Никакого требования чтобы источник был коллекцией в памяти нет.
В качестве примера смотрите ranges::getlines
, который итерирует по строкам файла.
Пример: читаем файл по строкам, пропускаем пустые и закомментированные строки, и парсим каждую строку в вектор даблов:
auto r = ranges::getlines(std::cin)
| ranges::view::remove_if(signal::io::empty_or_comment())
| ranges::view::transform(signal::io::x3_row_parser<double>());
(signal::io:: определены в моём проекте)
Ну и конечно же можно оперелить свои источники данных.
Основное отличие(если я правильно понимаю) в том, что LINQ предназначен для работы с разными источниками данных, а не только с коллекциями в памяти. Например есть провайдеры для работы с БД, xml, json и т.д.xml и json — те же самый коллекции в памяти, работающие с тем же самым IEnumerable, тут обычный linq2objects. А вот с базой там принципиально иной подход.
Суть в том, что шарповый LINQ умеет генерить, например, SQL-запросы из вашего кода. Сделано это за счёт того, что при использовании LINQ поверх IQueryable компилятором генерируется не код выражения, а код, создающий в памяти структуру под названием дерево выражений (expression tree), которую в дальнейшем LINQ-провайдер может анализировать. К примеру код
return q.Where(x => x.Bar == 1).ToList();
или эквивалентный ему
return (from x in q where x.Bar == 1 select x).ToList();
компилятор превратит в
ParameterExpression parameterExpression = Expression.Parameter(typeof(Program.Foo), "x");
return q.Where(Expression.Lambda<Func<Program.Foo, bool>>(Expression.Equal(Expression.Property(parameterExpression, methodof(Program.Foo.get_Bar())), Expression.Constant(1, typeof(int))), new ParameterExpression[]{parameterExpression})).ToList<Program.Foo>();
Соответственно LINQ-провайдер через рефлексию разберётся, к чему относится IQueryable и что есть Bar, и в дальнейшем сгенерит запрос к базе «SELECT * FROM Foos WHERE Bar = 1», который и выполнится при попытке перечислить IQueryable (что происходит при вызове ToList, который как раз работает через IEnumerable). Как это вообще на C++ переносить — мне не особо понятно. Как это вообще на C++ переносить — мне не особо понятно.
На чистые плюсы, насколько я понимаю, никак.
А в целом для построения какой-никакой рефлексии и вообще сохранения некоторой информации из исходного кода есть обычный подход — делаете внешнюю тулзу, которая умеет по некоторой разметке в исходном коде генерить нужную вам информацию, а потом встраиваете как шаг сборки. Так например делает Qt moc, он генерит реализацию методов рефлексии для наследников QObject'а в отдельный файл.
Если не хочется делать спец-разметку, то по-идее можно подумать насчет внешней тулзы на основе libclang или вообще плагина для clang'а.
Вариант с Ranges я рассмотрю. Спасибо.
На первый взгляд всё достаточно печально:
- Внезапные аллокации внутри обработчиков.
- Копирование класса
relinx_object
некорректно. - Не везде, где нужно, используется
std::forward
. - Всё в одном файле (как так-то?!).
К тому же невозможно скачать и сразу собрать проект (пришлось собирать вручную), а ещё куча предупреждений на самые простые флаги (-Wall -Wextra -Wpedantic
).
2. Что значит, копирование класса relinx_object? Конструктор копирования или что?
3. Я проверю на предмет forward. Мог пропустить где-то.
4. И что?
У clang-а вообще никаких претензий нет:
clang++.exe -Wall -std=c++14 -fexceptions -O3 -Wnon-virtual-dtor -Wbind-to-temporary-copy -Wambiguous-member-template -pedantic-errors -pedantic -Wall -target x86_64-w64-mingw32 -c D:\Projects\relinx\main.cpp -o obj\Release\main.o
clang++.exe -o bin\Release\relinx.exe obj\Release\main.o -s -target x86_64-w64-mingw32
Output file is bin\Release\relinx.exe with size 464.00 KB
Process terminated with status 0 (0 minute(s), 18 second(s))
0 error(s), 0 warning(s) (0 minute(s), 18 second(s))
Аллокации
Аллокации происходят, например, при каждом вызове конструктора класса
relinx_object
. Это функцияfrom
, а также всякие обработчики типаcast
,cycle
и т.д.
Копирование (и перенос тоже)
В языке C++ существует две операции копирования. Это конструктор копирования и оператор копирующего присвоения.
Класс
relinx_object
при конструировании создаёт ссылки на самого себя — итераторы на внутренний контейнер. Вызов генерируемых компилятором операций копирования (и переноса) по-умолчанию для такой структуры приведёт к созданию некорректного объекта.
Запустите у себя такой код:
const auto l = [] {return from({1, 2, 3, 4, 5});}; const auto y = l(); std::cout << y.sum() << std::endl;
2. Ваш код, с последними моими изменениями в коде relinx_object (где я удалил конструктор копирования), выводит результат 15.
я не считаю эти «аллокации» слишком большой проблемой
Значит, вы не пишете программы, требовательные к производительности.
И это самые настоящие аллокации, без кавычек.
выводит результат 15
Повезло. Как я уже писал, перенос по-умолчанию в вашем случае также некорректен.
Ну и простой запрет копирования — это, скорее, костыль, чем решение проблемы.
Запрет копирования — это не костыль, а by design.
Простите, но Ваша манера тыкать носом без объяснения причин выглядит не по-людски, я бы сказал даже агрессивно.
перенос по-умолчанию в вашем случае также некорректен
Ну так и поясните почему. Или я недостоин? :)
Везение тут не причём. Либо работает, либо нет.
Вы не сталкивались с ситуациями, когда объект недействителен, но к его данным всё ещё есть доступ? Висячие ссылки?
… без объяснения причин ...
Вообще-то я уже объяснил причину. Но давайте повторю.
Класс relinx_object при конструировании создаёт ссылки на самого себя.
Из этого моментально следует, что генерируемые компилятором операции копирования будут работать неправильно, потому что они реализуют почленное копирование. Следовательно, в скопированном объекте будут ссылки уже не на себя, а на копируемый объект.
То же самое относится и к операциям переноса.
В общем-то, это задача на собеседование для начинающего плюсовика, только на собеседованиях она обычно подаётся в виде "напишите конструктор копирования для класса, который управляет голым указателем".
Да, Вы правы насчёт ссылок. Поправил. Данные переносятся теперь в новые экземпляры relinx_object.
relinx_object(relinx_object &&) = default;
Если вы, возможно, не поняли в чем её проблема, то попробую объяснить ещё раз: не для всех контейнеров move-конструктор реализован, как простой swap указателей на данные. Соответственно, итераторы (которые у вас просто копируются при таком задании конструктора), указывающие на данные старого контейнере, могут не быть корректными итераторами для перемещенного контейнера. Самый простой пример: массивы статической длины, если у вас
Container
будет типа std::array<int>
, то перемещение этого массива — это просто побитовое копирование данных из старого объекта в новый, а, соответственно, _begin
и _end
нового объекта будут указывать внутрь старого массива. Если старый объект после этого уничтожается (например, он был временным объектом), то имеем use-after-free. Я бы накидал вам код для наглядности, но не смог быстро разобраться, что за новый параметр StoreType
вы добавили в последней редакции, надеюсь и так понятно.Причем из-за выбранного вами дизайна, когда у вас один и тот же объект может быть как простой невладеющей парой итераторов, так и владеть собственным контейнером, кроме того для второго случая не всегда
_begin
указывает на начало владеемого контейнера (т.к. он может быть сдвинут с помощью skip
, может и каких-то других функций), я не представляю как это можно легко исправить, не теряя при этом в эффективности (потому что понятно, что данные, лежащие по _begin
и _end
, всегда можно просто тупо скопировать).template<typename Container>
auto from(Container &&c) -> decltype(auto)
{
return relinx_object<typename std::decay<decltype(std::begin(c))>::type>(
std::begin(c), std::end(c));
}
Даже для prvalue аргумента (когда параметр выводится как rvalue ссылка
Container&&
) все равно используется невладеющий вип конструктора (по паре итераторов), соответственно, какой-нибудь такой код не сработает:vector<int> func();
auto r = from(func());
// ниже этой строчки r использовать нельзя,
// т.к. вектор, который вернула func(), уже уничтожен
Более того, версия
from
, принимающая std::initializer_list
, благодаря какой-то неочевидной шаблонной магии, вызывает вышеупомянутую версию функции, поэтому даже такой код не будет работать:auto r = from({1, 2, 3});
// дальше этой строчки r использовать нельзя
В общем, пока у вас все экземпляры
relinx_object
живут только до конца выражения, то всё хорошо, при более длительном времени жизни начинаются приключения.Вторая проблема — у вас внутри объекта лежит огромная куча каких-то непонятных данных, объект, создаваемый из простой пары указателей (
Iterator = T*
), занимает в памяти вместо ожидаемых 16 байт аж целых 128. Не знаю как насчет остальных, но _indexer
, _def_val_container
и _default_value
определенно лишние, они используются везде как типичные временные переменные:template<typename ForeachFunctor>
auto for_each_i(ForeachFunctor &&foreachFunctor) const noexcept -> void
{
auto begin = _begin;
auto end = _end;
_indexer = 0;
while (begin != end)
{
foreachFunctor(*begin, _indexer);
++_indexer;
++begin;
}
}
В чем смысл помещения их внутрь объекта я не понимаю.
Дальше:
template<typename AvgFunctor>
auto avarage(AvgFunctor &&avgFunctor) const noexcept -> decltype(auto)
{
return (sum(std::forward<AvgFunctor>(avgFunctor))
/ std::distance(_begin, _end));
}
Во-первых, конечно, average, во-вторых, в случае не
RandomAccessIterator
реализация неэффективна (два прохода вместо одного), а в случае InputIterator
(по которым можно сделать только один проход, например, std::istream_iterator
) вообще не сработает.Ну и по мелочи:
using self_type = relinx_object<Iterator, ContainerType>;
с последней редакцией указывает на неверный тип.
relinx_object(ContainerType &&container) noexcept
: _container(std::forward<ContainerType>(container))
, _begin(std::begin(_container))
, _end(std::end(_container)) {}
Напишите по-русски
_container(std::move(container))
, у вас тут аргумент нешаблонный, никакой тип не выводится.Спасибо большое за дельные советы и замечания. Попытаюсь в ближайшее время поправить.
Я, скорее всего, изменю архитектуру...
relinx_object
, а другие — меняют сам исходной объект (например, skip
, мб какие-то ещё). Я понимаю, что в случае skip
это вызвано идеей оптимизации, но выглядит такой интерфейс контринтуитивно.Обновил Relinx.
Манеры… Всем что-то и как-то не нравиться в этой жизни. Я считаю, главное в хороших манерах, подумать что и как ты говоришь. Взаимоуважение, я считаю, это необходимое качество для нормального общения. Но, по большей части, Ваши слова в комментариях имеют унизительный характер в мой адрес. Может Вы и отличный программист, а вот как человек в общении как-то не очень, извините.
Обновил Relinx
— Build: Release-GCC in relinx (compiler: GNU GCC Compiler)---------------
x86_64-w64-mingw32-g++.exe -Wall -std=c++14 -fexceptions -O3 -pedantic -Wextra -Wall -c D:\Projects\relinx\main.cpp -o obj\Release\main.o
x86_64-w64-mingw32-g++.exe -o bin\Release\relinx.exe obj\Release\main.o -s
Output file is bin\Release\relinx.exe with size 385.00 KB
Process terminated with status 0 (0 minute(s), 12 second(s))
0 error(s), 0 warning(s) (0 minute(s), 12 second(s))
— Закомител код.
Relinx — ещё одна реализация .NET LINQ методов на C++, с поддержкой «ленивых вычислений»