Как стать автором
Обновить

Комментарии 25

А есть какие-либо принципиальные идейные отличия между LINQ и Ranges? Если нет, то не логичнее ли (если уж выбрали путь идеоматичности) использовать Ranges, тем более они претендуют на включение в стандарт?

Основное отличие(если я правильно понимаю) в том, что LINQ предназначен для работы с разными источниками данных, а не только с коллекциями в памяти. Например есть провайдеры для работы с БД, xml, json и т.д. Плюс вы можете написать свой провайдер для нужного источника данных.

В 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'а.
Судя по исходникам, from возвращает тот же диапазон, но алгоритмы работы с ним жестко зашиты в его методах, и предназначены только для forward-only/input iterators. Концепция Ranges в этом плане более универсальна, ограничения на итераторы задается на уровне алгоритма, а не диапазона. И синтаксис подобный синтаксису из статьи поддерживается: auto res = data | transform([](auto&&){...})
Спасибо за информацию… Я рассмотрю Ranges как вариант…
Я хотел написать именно LINQ-подобную библиотеку, для переноса LINQ выражений из C# в C++ и наоборот.
Вариант с Ranges я рассмотрю. Спасибо.

На первый взгляд всё достаточно печально:


  1. Внезапные аллокации внутри обработчиков.
  2. Копирование класса relinx_object некорректно.
  3. Не везде, где нужно, используется std::forward.
  4. Всё в одном файле (как так-то?!).

К тому же невозможно скачать и сразу собрать проект (пришлось собирать вручную), а ещё куча предупреждений на самые простые флаги (-Wall -Wextra -Wpedantic).

1. Укажите, пожалуйста, где происходят внезапные аллокации, чтобы, если это верно, я мог поправить код.
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))
  1. Аллокации


    Аллокации происходят, например, при каждом вызове конструктора класса relinx_object. Это функция from, а также всякие обработчики типа cast, cycle и т.д.


  2. Копирование (и перенос тоже)


    В языке C++ существует две операции копирования. Это конструктор копирования и оператор копирующего присвоения.


    Класс relinx_object при конструировании создаёт ссылки на самого себя — итераторы на внутренний контейнер. Вызов генерируемых компилятором операций копирования (и переноса) по-умолчанию для такой структуры приведёт к созданию некорректного объекта.


    Запустите у себя такой код:


    const auto l = [] {return from({1, 2, 3, 4, 5});};
    const auto y = l();
    
    std::cout << y.sum() << std::endl;

1. Я понял Вас. Спасибо, поправлю. Но, тем не менее, я не считаю эти «аллокации» слишком большой проблемой. Просто, простор для оптимизации ;) Хотя, Ваше замечание считаю было по делу )

2. Ваш код, с последними моими изменениями в коде relinx_object (где я удалил конструктор копирования), выводит результат 15.
я не считаю эти «аллокации» слишком большой проблемой


Значит, вы не пишете программы, требовательные к производительности.
И это самые настоящие аллокации, без кавычек.

выводит результат 15


Повезло. Как я уже писал, перенос по-умолчанию в вашем случае также некорректен.
Ну и простой запрет копирования — это, скорее, костыль, чем решение проблемы.
Везение тут не причём. Либо работает, либо нет.
Запрет копирования — это не костыль, а by design.

Простите, но Ваша манера тыкать носом без объяснения причин выглядит не по-людски, я бы сказал даже агрессивно.
перенос по-умолчанию в вашем случае также некорректен


Ну так и поясните почему. Или я недостоин? :)
Везение тут не причём. Либо работает, либо нет.


Вы не сталкивались с ситуациями, когда объект недействителен, но к его данным всё ещё есть доступ? Висячие ссылки?
… без объяснения причин ...


Вообще-то я уже объяснил причину. Но давайте повторю.

Класс relinx_object при конструировании создаёт ссылки на самого себя.

Из этого моментально следует, что генерируемые компилятором операции копирования будут работать неправильно, потому что они реализуют почленное копирование. Следовательно, в скопированном объекте будут ссылки уже не на себя, а на копируемый объект.

То же самое относится и к операциям переноса.

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


Манеры
Если говорить о манерах, то мне, например, не нравится манера огрызаться вместо того, чтобы пять минут подумать.

Да, Вы правы насчёт ссылок. Поправил. Данные переносятся теперь в новые экземпляры relinx_object.

В репозитории на github у вас по-прежнему:
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 это вызвано идеей оптимизации, но выглядит такой интерфейс контринтуитивно.
а как по вашему мнению обстоят дела с владением у Ranges?

Обновил Relinx.

Манеры

Манеры… Всем что-то и как-то не нравиться в этой жизни. Я считаю, главное в хороших манерах, подумать что и как ты говоришь. Взаимоуважение, я считаю, это необходимое качество для нормального общения. Но, по большей части, Ваши слова в комментариях имеют унизительный характер в мой адрес. Может Вы и отличный программист, а вот как человек в общении как-то не очень, извините.

Обновил Relinx

Кстати, GCC с флагами -Wall -Wextra -Wpedantic ругался только 'warning: unused parameter 'v' [-Wunused-parameter]' с сопутствующей кучей воды. Я убрал неиспользуемые параметры и вуаля:

— 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))

— Закомител код.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории