cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

добавить возможность передать владение указателем в/из `std::string` и `std::vector` #537

Open sigasigasiga opened 1 year ago

sigasigasiga commented 1 year ago

C++ хорош тем, что он позволяет без лишней возни и оверхеда использовать C'шные библиотеки. однако в некоторых сценариях межъязыковое взаимодействие всё-таки даёт о себе знать: предположим, C'шная библиотека возвращает какой-то буфер, владение которым отдаётся на откуп пользователю, а затем этот буфер надо передать уже в плюсовую библиотеку, которая принимает какой-то contiguous контейнер (например, std::vector или std::string). в этом случае не видится никакого другого решения, кроме как скопировать буфер из C'шной библиотеки в контейнер, а затем его удалить. копирование на пустом месте!

примерно та же история может быть работать в обратную сторону: может потребоваться отдать в C'шную библиотеку владение указателем, который принадлежит std::vector'у или std::string'у, и в этом случае тоже придётся делать абсолютно бессмысленное копирование.

предлагается дать возможность контейнерам более гибко работать с владением буферами.

передача владения в контейнер наверное, стоит сделать новый конструктор от четырёх аргументов: pointer, size, capacity и allocator -- такой вариант конструктора, насколько я вижу, ещё никем не использован. возможно, лучше бы было добавить статический метод в класс, чтобы его имя лучше отражало суть происходящего, однако судя по тому, что в комитете ни разу подобным образом не поступили, такой вариант вряд ли прийдётся кому-то по душе.

передача владения из контейнера для этого, думаю, стоит добавить метод release по аналогии с тем, что сейчас есть в std::unique_ptr. возвращать он может какую-то структуру (но ни в коем случае не std::tuple, использование которого обречёт пользователей на написание std::get</* SOME MAGIC NUMBER */>), которая будет содержать указатель, размер, capacity и аллокатор.

аллокаторы эти изменения вряд ли будет работать так просто -- в C'шном коде в лучшем случае память выделяется с помощью malloc'а и удаляется с помощью free, а std::allocator делает это с помощью new и delete соответственно, и между собой эти два варианта аллокации/деаллокации, к сожалению, несовместимы. поэтому, скорее всего, нужно будет добавить в стандартную библиотеку ещё один вариант аллокатора, который использует malloc и free (а назвать его можно mallocator'ом:) ).

sergii-rybin-tfs commented 1 year ago

чем плохи std::string_view и std::span ?

sigasigasiga commented 1 year ago

@sergii-rybin-tfs они не освободят ресурс при вызове деструктора

sergii-rybin-tfs commented 1 year ago

ясно, я бы завернул пару string_view+unique_ptr например в tuple<unique_ptr,string_view>.

Можно добавить читабельный алиас и оператор неявного преобразования типа и deduction guide.

pavelkryukov commented 1 year ago

в плюсовую библиотеку, которая принимает какой-то contiguous контейнер (например, std::vector или std::string)

ещё один вариант аллокатора, который использует malloc и free

но контейнер с другим аллокатором – это другой контейнер, который нельзя будет передать в плюсовую библиотеку, если она принимает обычные std::string или std::vector.

sigasigasiga commented 1 year ago

@pavelkryukov возможно, тут моя аргументация даёт течь. попробую привести другой пример, который нередко встречается в жизни: мы пишем плюсовую обёртку вокруг C'шной бибилотеки.

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

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

ко всему прочему, проблема необходимости привязки к конкретным аллокаторам в случаях, когда это совершенно не важно, была адресована появлением std::pmr::vector

sigasigasiga commented 1 year ago

придумал ещё один вариант использования предлагаемой мною фичи: можно будет конвертировать std::vector в std::string совершенно бесплатно и без лишних мучений.

sigasigasiga commented 1 year ago

ещё один пример, где это было бы очень полезно. представим, нам нужно передать владение указателем на массив в C'шную библиотеку, и перед вызовом нам надо этот массив как-то подготовить (т.е. заполнить какими-то данными). с предлагаемыми изменениями мы просто можем завести std::vector<char, mallocator>, заполнить его в привычной для C++ программиста манере, а затем release'нуть подготовленный массив, и передать его туда куда надо

pavelkryukov commented 1 year ago

нам нужно передать владение указателем на массив в C'шную библиотеку, и перед вызовом нам надо этот массив как-то подготовить (т.е. заполнить какими-то данными).

Это решается через std::span:

auto* ptr = (struct char*)malloc(sizeof(char) * size);
std::span<char> span(ptr, size);
fill_span(span.begin(), span.end()); // заполнение в привычной для C++ манере
pass_to_c_library(ptr);

придумал ещё один вариант использования предлагаемой мною фичи: можно будет конвертировать std::vector в std::string совершенно бесплатно и без лишних мучений.

Возможно, будут проблемы, если std::string имплементирован через SSO. Плюс, мне кажется, должно хватать конвертации владеющих типов в невладеющие: std::string_view или std::span. Если кто-то принимает объект владеющего класса, то без копии скорее всего не обойтись.

P.S. что будет с нуль-терминированием строк, созданных из std::vector?

pavelkryukov commented 1 year ago

Ещё вариант — использовать std::unique_ptr<T[]> или std::shared_ptr<T[]> с кастомным deleter-ом

sigasigasiga commented 1 year ago

Это решается через std::span:

не решается. у вектора есть возможность удобно расширять размер буфера, у спана такого нет

Возможно, будут проблемы, если std::string имплементирован через SSO.

в таком случае можно выделять буфер в куче, всё туда копировать и возвращать указатель на него. таким образом мы, к сожалению, теряем noexcept, но это, наверное, не так страшно

P.S. что будет с нуль-терминированием строк, созданных из std::vector?

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

Ещё вариант — использовать std::unique_ptr<T[]> или std::shared_ptr<T[]> с кастомным deleter-ом

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

incoder1 commented 1 year ago

Внутри std::stirng вообще может не быть указателя, изза short string optimization. https://stackoverflow.com/questions/21694302/what-are-the-mechanics-of-short-string-optimization-in-libc Поэтому хакнуть дизайн STL изначально разработанной под deep copy не выйдет. Если нужно copy on write - его надо делать самостоятельно. Для общего случая ввели универстальный класс std::string который скорее для имитации строк других языков, т..е. для функциональности на все случаи жизни.

sigasigasiga commented 1 year ago

@incoder1 на аргумент про SSO я уже ответил вот тут. а при чём тут copy-on-write я, честно говоря, не понял

incoder1 commented 1 year ago

@sigasigasiga передача владения из контейнера это вариант copy-on-write с shallow copy вместо обычного deep copy. Изначальный дизайн STL - дополнить язык выско-уровневыми типами данных, аналогичными другим языкам с помощью шаблонов и ООП. В частности строками (в С и голом С++ есть только массивы символов), динамическими и ассоциативными массивами. То что вы предлагаете - это другой дизайн.

sigasigasiga commented 1 year ago

@incoder1

передача владения из контейнера это вариант copy-on-write с shallow copy вместо обычного deep copy.

честно говоря, я всё ещё не понимаю, при чём тут copy-on-write. ведь cow -- это, по своей сути, просто отложенное копирование объекта, позволяющее делать копию не сразу, а в момент, когда она действиельно потребуется. то, что предлагаю я, с копированием напрямую никак не связано.

То что вы предлагаете - это другой дизайн.

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

incoder1 commented 1 year ago

В целом про демагогию согласен. Договорится за дизайн очень сложно, противоборствующие лагеря с конфликтом интересов найдут миллион аргументов с каждой стороны не приходить к консенсусу. Примерно по этой причине 3 года не могут согласовать пулы потоков. Тут дело в следующем дизайну STL Cтепанова и Ли уже 29 лет, на нем завязано много существующего кода, трюков - гавный из который - взятие адреса нулевого элемента из контейнера для получения указателя, скажем классический трюк для преобразования в UTF-16LE

#include <windows.h>

#include <iostream>
#include <string>

std::wstring transcode(const char* src)
{
    int len = ::MultiByteToWideChar(CP_UTF8, 0, src, -1, nullptr, 0);
    std::wstring ret(len, L'\0');
    ::MultiByteToWideChar(CP_UTF8, 0, src, -1, &ret[0], len);
    return ret;
}

int main(int argc, const char** argv)
{
    std::wcout << ::transcode("Hello world!") << std::endl;
    return 0;
}

Под эти трюки даже были дополнения в стандарт, например стандарт требует чтобы блок памяти под строку в векторе и строке был сплошным. Ваше предложение - хакнуть дизайн и возможно сломать все эти воркераунды миллионах строк кода тысячь проектов созданных за эти почти 30 лет. Что будет если мы изъяли владение указателем на блок памяти из контейнера, а потом кусок кода начинает обращаться к адресу по нулевого элемента этого контейнера ? ИМХО - проще создать вообще праралельную библиотеку контейнеров которая по другому обращается с памятью, в частности ведут себя как скажем строки Java и C# - множество интеллектуальных указателей на один массив, copy-on-write и т.д. а существующие контейнеры оставить в покое.

sigasigasiga commented 1 year ago

@incoder1 вы говорите чересчур абстрактно. можете, пожалуйста, привести какую-нибудь конкретную ситуацию, где предлагаемые мною изменения что-то сломают в существующем коде?

взятие адреса нулевого элемента из контейнера для получения указателя

предлагаемые мною изменения это поведение не меняют

Ваше предложение - хакнуть дизайн и возможно сломать все эти воркераунды миллионах строк кода тысячь проектов созданных за эти почти 30 лет

почему же? если в древний код не добавлять использование предлагаемого мною метода release, то он будет работать как прежде.

Что будет если мы изъяли владение указателем на блок памяти из контейнера, а потом кусок кода начинает обращаться к адресу по нулевого элемента этого контейнера ?

UB, точно так же, как если мы будем разыменовывать unique_ptr, у которого забрали владение указателем.