В основных рекомендациях C++ много раз говорится, что использование void*
в качестве аргумента в лучшем случае сбивает с толку, а в худшем — чревато ошибками. Мое любимое упоминание в конце:
Основные рекомендации C++: задачи: неклассифицированные прото-правила
Anyone writing a public interface which takes or returns void* should have their toes set on fire. That one has been a personal favorite of mine for a number of years. :)
Это сказало:
На что следует изменить сигнатуру этой функции, чтобы выполнить это предложение? В настоящее время он работает со всем, что можно интерпретировать как const char*
:
bool writeBufferToFile(void* buffer, std::size_t size, const std::string& filePath) const
{
namespace FS = std::filesystem;
FS::path p(filePath);
p.make_preferred();
bool not_valid_path = FS::is_directory(p);
bool invalid = not_valid_path;
if (invalid) { return false; }
std::ofstream ofs;
ofs.open(p.string(), std::ios_base::binary);
if (ofs)
{
ofs.write(reinterpret_cast<const char*>(buffer), size);
return true;
}
return false;
}
Я считаю, что комментарий, который вы цитируете, нацелен на интерфейсы, которые пытаются использовать void*
для достижения полиморфизма, для которого C++ имеет лучшие и более безопасные инструменты способ. В контексте работы с простыми старыми двоичными данными имеет смысл необработанный указатель + размер. Просто посмотрите на сигнатуру функции, которая в конечном итоге выполняет вашу работу: basic_ostream::write(const char_type* s, std::streamsize count)
Вам нужны не два параметра, указатель и размер, а один параметр, представляющий двоичный фрагмент данных. В идеальном мире вы могли бы использовать что-то вроде std::vector<std::uint8_t> const&
, но проблема в том, что это заставляет вызывающих абонентов выполнять выделение/копирование/освобождение, если они хранят данные каким-то другим способом.
Итак, что вам действительно нужно, так это класс, который представляет двоичный блок данных, но не владеет этими данными и может быть сконструирован, скопирован и уничтожен очень дешево, независимо от того, как хранятся базовые данные. Это позволяет избежать необходимости иметь два параметра для выражения одной концепции.
Класс, который инкапсулирует это, часто называют Slice
. Итак, я бы предложил:
class Slice
{
private:
std::uint8_t const* data_;
std::size_t size_;
...
};
bool writeBufferToFile (const Slice& data, const std::string& filePath) const
Ваш класс Slice
может быть легко создан из std::vector <std::uint8_t>
или практически любого другого разумного способа хранения диапазона байтов.
Но это просто перенос проблемы на один из Нарезные конструкторы! :/
Это просто заметание грязи под ковер. Общий указатель и размер в красивой рамке по-прежнему являются общим указателем и размером. Коробка не решает никаких проблем.
std::uint8_t
не гарантируется, что это какой-то тип char, подходящий для псевдонима любого типа. Это может быть UB на некоторых платформах
@н.м. Это решает проблему необходимости разбивать одну вещь (такую как вектор, массив и т.п.) на две вещи с запутанным кодом в каждом месте вызова. С соответствующими конструкторами и другим кодом вызовы writeBufferToFile
будут выглядеть чистыми везде, и не будет риска несоответствия параметров.
@Casey Вы можете использовать этот конструктор, если у вас нет лучшего выбора. Суть в том, чтобы предоставить лучший выбор. (Конечно, вам, возможно, все же придется взаимодействовать с людьми, которые не сделали лучший выбор.)
Я выбрал std::any
. Его можно использовать как безопасную замену void*
.
Мотивирующие статьи:
bool WriteBufferToFile(const std::any& buffer, std::size_t size, std::filesystem::path filepath) noexcept {
namespace FS = std::filesystem;
filepath = FS::absolute(filepath);
filepath.make_preferred();
const auto not_valid_path = FS::is_directory(filepath);
const auto invalid = not_valid_path;
if (invalid) {
return false;
}
if (std::ofstream ofs{filepath, std::ios_base::binary}; ofs.write(reinterpret_cast<const char*>(&buffer), size)) {
return true;
}
return false;
}
Это перебор,
void*
есть причина.