Описание информации о формате пикселей в С++ способом, который можно использовать как во время компиляции, так и во время выполнения
У меня есть библиотека, которая выполняет операции с пикселями. Пиксели могут использоваться во многих разных форматах. Я ищу эффективный способ описания форматов в библиотечном API (внутри и снаружи).
Для некоторых классов формат пикселей является аргументом шаблона, для других это аргумент времени выполнения. Таким образом, форматы пикселей должны использоваться как во время выполнения (как конструктор, так и в аргументе функции) и времени компиляции (в качестве аргумента шаблона). Я хочу описывать пиксельные форматы только один раз.
Теперь у меня есть что-то вроде этого:
enum class color_space : uint8_t { rgb, cmyk /* , etc... */ };
struct pixel_layout {
color_space space;
uint8_t channels;
/* etc... */
};
template <color_space ColorSpace, uint8_t Channels /* etc.. */>
struct pixel_type {
static constexpr color_space space = ColorSpace;
static constexpr uint8_t channels = Channels;
/* etc... */
static constexpr pixel_layout layout() {
return {space, channels /* , etc... */ };
}
};
struct rgb : public pixel_type<color_space::rgb, 3 /* , etc... */ > {};
struct rgba : public pixel_type<color_space::rgb, 4 /* , etc... */ > {};
Это работает довольно хорошо. Я могу использовать их как параметры времени выполнения и компиляции:
template <class PixelType>
class image { };
struct transform {
transform(const pixel_layout from, const pixel_layout to)
: from(from), to(to) { /* ... */ }
pixel_layout from;
pixel_layout to;
};
Также конвертировать из типа времени компиляции в тип выполнения:
transform(rgb::layout(), rgba::layout());
Однако, дублирование и сохранение деталей pixel_layout
типов пикселей, когда они используются во время выполнения, кажутся мне глупыми. Концептуально для всей программы необходимо указать ID/адрес/ссылку на конкретный pixel_type
и способ получить связанные свойства (цветовое пространство, каналы и т.д.) Во время компиляции и времени выполнения.
Кроме того, если я хочу получить производное свойство из типа пикселя, мне нужно реализовать его на pixel_layout
, если я хочу избежать дублирования логики. Затем, чтобы использовать его во время компиляции, мне нужно перейти от класса pixel_type<...>
к экземпляру pixel_layout
к производному свойству. Это тоже кажется немного глупым.
Могу ли я обойти детали pixel_layout
и вместо этого использовать некоторую ссылку на классы pixel_type<...>
(sub)?
Я попытался использовать enum
s, потому что перечисления работают как аргумент шаблона и аргумент функции. Но я изо всех сил пытался получить значение enum (например, rgba
) для свойства типа пикселя (например, 4 канала) во время выполнения и время компиляции в идиоматическом режиме С++.
Кроме того, перечисления как аргументы шаблона дают гораздо менее полезную диагностику во время ошибки компиляции. Например, я получаю image<(pixel_type)2>
, а не image<rgba>
в компиляции сообщений об ошибках с clang. Таким образом, это не похоже на полезный подход.
Ответы
Ответ 1
Использование параметров шаблона нестандартного шаблона может быть решением. См. http://en.cppreference.com/w/cpp/language/template_parameters. Например, например:
#include <iostream>
#include <cstdint>
#include <array>
enum class color_space : std::uint8_t { rgb, cymk, other };
// PIXEL LAYOUT
// Can be created/modified at runtime, but a predefined set of pixel_layouts
// exists for compile-time use.
struct pixel_layout {
color_space space;
std::uint8_t channels;
};
constexpr bool operator==(const pixel_layout& a, const pixel_layout& b) {
return (a.space == b.space) && (a.channels == b.channels);
}
constexpr bool operator!=(const pixel_layout& a, const pixel_layout& b) {
return (a.space != b.space) || (a.channels != b.channels);
}
// Predefined pixel_layout instances, for use as template arguments
// As static constexpr members of class, to make sure they have external linkage,
// required for use as reference template arguments.
struct default_pixel_layouts {
static constexpr pixel_layout rgb{ color_space::rgb, 3 };
static constexpr pixel_layout cymk{ color_space::cymk, 4 };
};
// Definitions for the pixel_layouts
constexpr pixel_layout default_pixel_layouts::rgb;
constexpr pixel_layout default_pixel_layouts::cymk;
// PIXEL TYPE
// Takes pixel_layout reference as non-type template argument.
template<const pixel_layout& Layout>
struct pixel {
static constexpr const pixel_layout& layout = Layout;
// Because layout is constexpr, can use its members (e.g. channels),
// for example as template argument.
// Here size of pixel depends on number of channels in pixel_layout
std::array<std::uint32_t, layout.channels> data;
};
// RGB and CYMK pixel_types as type aliases
using rgb = pixel<default_pixel_layouts::rgb>;
using cymk = pixel<default_pixel_layouts::cymk>;
// IMAGE
// Takes pixel type as template argument.
template<class PixelType>
class image {
public:
using pixel_type = PixelType;
};
// TRANSFORM
// Takes pixel_layouts to transform from/to at runtime. Can for with the predefined
// ones, but also with new ones creates at runtime.
class transform {
private:
const pixel_layout& from_;
const pixel_layout& to_;
public:
transform(const pixel_layout& from, const pixel_layout& to) :
from_(from), to_(to) { }
// Example: function working on an image
template<class Image>
void run(Image& img) {
// Need to make sure that Image pixel_layout (compile-time) matches
// pixel_layout of the transform (runtime).
if(Image::pixel_type::layout != from_)
std::cout << "Wrong pixel type on input image" << std::endl;
else
std::cout << "transforming..." << std::endl;
}
};
int main() {
image<rgb> rgb_img;
image<cymk> cymk_img;
// Transform from rgb to cymk
transform tr(default_pixel_layouts::rgb, default_pixel_layouts::cymk);
tr.run(rgb_img); // ok
tr.run(cymk_img); // error: input to run() must have rgb pixel_layout
// Creating a new pixel_layout at runtime
pixel_layout custom_layout = { color_space::other, 10 };
transform tr2(custom_layout, default_pixel_layouts::cymk);
return 0;
}
http://coliru.stacked-crooked.com/a/981e1b03b3b815c5
В случаях использования, когда pixel_layout
используется во время компиляции, различные доступные экземпляры pixel_layout
должны быть созданы как глобальные статические объекты constexpr.
pixel_type
затем создает экземпляры для разных классов, в зависимости от pixel_layout&
, заданных как аргумент шаблона.
Но они все равно могут использоваться и во время выполнения.
Ответ 2
Я бы начал с создания rgb
и rgba
и т.д. пустых классов:
struct rgb{};
struct rgba{};
struct cmyk{};
//...
Используйте их вместо перечисления для шаблонов, чтобы улучшить диагностику.
Затем вы можете определить набор бесплатных constexpr
функций, которые выдают конкретные данные, например
constexpr uint8_t channels(rgb) { return 3; }
constexpr uint8_t channels(rgba) { return 4; }
Таким образом, вы можете предложить значения, которые имеют смысл для любого формата. Если функция недоступна для определенного формата, просто не предлагайте перегрузку.
Вы можете построить pixel_layout
, используя такие функции там, где это необходимо, но я бы предположил, что такие классы, как transform
, просто получат конструктор шаблона и собирают необходимую информацию без промежуточного элемента pixel_layout
.
Возможно, мне не хватает некоторых вариантов использования, поэтому вам может понадобиться немного изменить это значение, но я надеюсь, что это поможет.
Ответ 3
Соответствующий вам вопрос:
Могу ли я обойти детали pixel_layout
?, а вместо этого использовать какую-то ссылку на классы pixel_type<...>
(sub)?
Да, вы можете использовать наследование. Вы хотите выразить общий интерфейс во время выполнения, поэтому вам нужно, по крайней мере, один элемент данных указать разницу, как вы уже упоминали. Интерфейс будет выглядеть так:
struct pixel_layout {
virtual ~pixel_layout() = default;
virtual color_space colorSpace() const = 0;
virtual uint8_t channelCount() const = 0;
};
Пока так хорошо. Теперь мы можем передать указатель или ссылку на реализацию pixel_layout
и использовать его информацию для стоимости двух указателей. Один указывает на фактический объект, а другой на таблицу vtable.
Давайте сделаем реализацию.
template<typename _PixelT>
struct pixel_layout_implementation : pixel_layout {
virtual color_space colorSpace() const override { return _PixelT::colorSpace(); }
virtual uint8_t channelCount() const override { return _PixelT::channelCount(); }
};
Хорошо, мы получили это. Обратите внимание, что моя декларация pixel_type
немного отличается.
template<color_space _SpaceT, uint8_t _ChannelC>
struct pixel_type {
static constexpr color_space colorSpace() { return _SpaceT; }
static constexpr uint8_t channelCount() { return _ChannelC; }
};
Теперь мы можем обновить ваш класс преобразования.
struct transform {
transfrom(const pixel_layout& from, const pixel_layout& to);
};
Наконец мы попадаем в беду. У нас есть ссылки на наш pixel_layout
и до тех пор, пока они остаются в памяти, все прекрасно, но если мы хотим их скопировать, нам нужно реализовать глубокую копию и повторить несколько экземпляров. Прежде чем обращаться к этой проблеме, попробуйте скрыть некоторую сложность как деталь реализации pixel_layout
.
struct pixel_layout {
private:
struct concept_t {
virtual ~concept_t () = default;
virtual color_space colorSpace() const = 0;
virtual uint8_t channelCount() const = 0;
};
template<typename _PixelT>
struct concept_implementation_t : concept_t {
virtual color_space colorSpace() const override { return _PixelT::colorSpace(); }
virtual uint8_t channelCount() const override { return _PixelT::channelCount(); }
};
std::unique_ptr<const conept_t> pm_conceptImpl;
public:
template<typename _PixelT>
pixel_layout(_PixelT)
: pm_conceptImpl{new concept_implementation_t<_PixelT>}
{/* */}
virtual color_space colorSpace() const { return pm_conceptImpl->colorSpace(); }
virtual uint8_t channelCount() const { return pm_conceptImpl->channelCount(); }
};
Это делает наследование невидимым для пользователя этого класса, а unique_ptr неявно удаляет конструкцию и назначение копии. Кроме того, если pixel_type
является пустым классом, конструктор позволяет нам написать что-то вроде pixel_layout rgb_layout = rgb();
без утечки производительности. Давайте рассмотрим предыдущую проблему, которую мы еще не решили. Мы создаем новые объекты concept_implementation
для каждого pixel_layout
, каждый из которых указывает на ту же таблицу vtable. Мы можем сделать лучше, разделив один объект на каждый color_space
на несколько экземпляров.
template<typename _PixelT>
struct concept_implementation_t : concept_t {
static const std::unique_ptr<const concept_t> shared_instance;
virtual color_space colorSpace() const override { return _PixelT::colorSpace(); }
virtual uint8_t channelCount() const override { return _PixelT::channelCount(); }
};
Теперь нам нужно указать только shared_instance
в пределах нашего pixel_layout
.
const concept_t& pm_conceptImpl;
public:
template<typename _PixelT>
pixel_layout()
: pm_conceptImpl{*concept_implementation_t<_PixelT>::shared_instance.get()}
{/* */}
Собирая все вместе, мы получаем размер одного указателя для каждого экземпляра pixel_layout
плюс один указатель для каждого поддерживаемого pixel_type
. Вот последний класс и не забудьте поставить инициализацию статического члена после определения класса.
struct pixel_layout {
private:
struct concept_t {
virtual ~concept_t () = default;
virtual color_space colorSpace() const = 0;
virtual uint8_t channelCount() const = 0;
};
template<typename _PixelT>
struct concept_implementation_t : concept_t {
static const std::unique_ptr<const concept_t> shared_instance;
virtual color_space colorSpace() const override { return _PixelT::colorSpace(); }
virtual uint8_t channelCount() const override { return _PixelT::channelCount(); }
};
const concept_t& pm_conceptImpl;
public:
template<typename _PixelT>
pixel_layout(_PixelT)
: pm_conceptImpl{*concept_implementation_t<_PixelT>::shared_instance.get()}
{/* */}
virtual color_space colorSpace() const { return pm_conceptImpl.colorSpace(); }
virtual uint8_t channelCount() const { return pm_conceptImpl.channelCount(); }
};
template<typename _PixelT>
const std::unique_ptr<const pixel_layout::concept_t> pixel_layout::concept_implementation_t<_PixelT>::shared_instance
(new pixel_layout::concept_implementation_t<_PixelT>);
Ответ 4
Вот попытка. Может быть, мне что-то не хватает, но кажется, что вы можете сделать большую часть того, чего хотите, без структуры pixel_layout
.
Я бы использовал ту же реализацию для pixel_type
, поэтому:
enum class color_space : uint8_t { rgb, rgba };
template <color_space ColorSpace, uint8_t Channels>
struct pixel_type {
static constexpr color_space space = ColorSpace;
static constexpr uint8_t channels = Channels;
};
Но я бы использовал псевдоним для определения разных типов, например:
using rgb = pixel_type<color_space::rgb, 3>;
using rgba = pixel_type<color_space::rgba, 4>;
Я обнаружил, что использование псевдонима позволяет легко (er) расшифровать сообщения компилятора (особенно с clang), поэтому я решил использовать их здесь.
Теперь все, что я вижу, что функция layout
делает, - это обертывание информации, которую вы уже имеете в качестве параметров шаблона (color_space
и uint8_t
) в структуру. Однако вы можете получить доступ к этой информации с помощью шаблонов.
Я собираюсь предположить, что класс transform
делает что-то вроде этого (пожалуйста, поправьте меня, если я ошибаюсь):
class transform {
transform(const pixel_layout from, const pixel_layout to)
: from(from), to(to) {}
// Here *to* and *from* are stored so that to.space, from.space etc
// can be used for the transformation
};
Но вы можете сделать это (довольно красиво), сделав этот класс преобразования классом шаблона и передав типы преобразования To и From в качестве параметров шаблона. Это дает возможность разрешить доступ как к параметрам шаблона времени компиляции, так и к любым параметрам времени выполнения, например:
template <typename From, typename To>
class transform {
transform(const From& from, const To& to) {
// Access with From::channels, To::space etc, as the other transform class would have
// Access any run-time parameters with to.rtime_param etc...
}
}
Или, я думаю, это лучшее решение (но я не знаю конкретного использования, а может и нет), использовать функцию шаблона и возвращать преобразованный тип, например:
template <typename From, typename To>
To transform(const From& from, const To& to) {
// Same access, From::channels
// to.rtime_param
}
Кроме того, я считал, что некоторые преобразования могут быть специфичными для разных форматов пикселей (rgb → rgba может отличаться от rgb → cmyk), поэтому я бы просто предоставил специализации класса или функции преобразования для обработки этих конкретных случаев.
Вот пример возможного использования с моим решением:
rgb rgb_format;
rgba rgba_format;
// Using the transform struct
transform rgb_to_rgba(rgb_format, rgba_format);
// Or similarly using the function
rgba transformed_rgb_format = transform(rgb, rgba);
Кроме того, что касается добавления свойств, поскольку этот метод имеет доступ как к свойствам времени компиляции, так и к свойствам времени выполнения, любые добавления к структуре pixel_type могут быть легко доступны либо во время компиляции, либо во время выполнения.
Одной из потенциальных проблем является доступ к статическим компонентам вне шаблонных функций и класса, поскольку к ним нужно обращаться через тип, а не экземпляр.
rgb _rgb;
// Need to do this
rgb::channels
// Rather than this
_rgb::channels
Но я думаю, что проблема с читабельностью больше, чем та, которая влияет на функциональность.
Простая демонстрация
Ответ 5
Решение, с которым я столкнулся, включает в себя три структуры данных:
- Просмотр аннотации типов:
enum class pixel_type : char { };
- Тип объединения, содержащий данные пикселя:
union pixel_data { };
- Структуру для хранения обоих параметров с параметром шаблона для указания аннотации типа по умолчанию:
struct pixel{ };
Время компиляции/статическое:
- Используйте аргумент шаблона, чтобы установить тип формата, если вы его уже знаете.
- Как объединение,
pixel_data
будет несколько "без единиц". Если вы уже знаете тип, вы можете написать функции, которые делают предположение, что данные находятся в правильном представлении. Если вам требуется переменный объем памяти для хранения разных форматов данных пикселей, это явно проблема, которую стоит явно рассмотреть. Если это так, измените класс pixel
, чтобы параметризовать внутреннюю реализацию, которая отклоняется в зависимости от размера представления пиксельной памяти.
- Используйте класс
pixel
во время выполнения для данных, которые должны быть включены в тип. После этого вы можете развернуть аннотацию типа, используя только ее член pixel_data
, а затем выполните преобразования. Разделяя фактический базовый тип данных и метаданные, должно быть проще выполнить вычисление данных с массой данных без необходимости переходить через функции-члены, принадлежащие только классу, что может стать затруднительным, если вы решите пойти на многопоточность. Фактически, я бы сказал, что вы также можете включить блокировку в классе пикселей, если вы беспокоитесь об этом или даже примените его к базовым данным. Это зависит от вас.
- Используйте
pixel_data
для вычисления в полях кода, где можно предположить тип; в противном случае рассматривайте класс pixel
как своего рода вариант или необязательный тип данных.
Время выполнения/динамическое
- Используйте
pixel_data::unknown
для обеспечения безопасности во время выполнения. Если вы этого желаете, вы можете создать своего рода функцию apply
, которая будет действовать как монадический защитник, чтобы предотвратить прохождение форматов NULL
-like.
- Задайте формат в структуре
pixel
, как только вы выберете формат.
- Еще раз вы можете удалить обертку
pixel
и выбросить аннотацию типа, как только вы захотите во время вычисления.
Массовое хранение пикселей и вычисление/преобразование
-
Создайте класс контейнера с аннотацией типа; это должно содержать много типов данных, все из которых имеют одинаковый тип данных; трата байт на пиксель для хранения формата пикселей является расточительной; если вы хотите хранить матрицу, просто сохраните один байт для всей матрицы и снова настройте на фактический базовый тип данных. Это должно быть своего рода pixel_matrix
, а основная структура памяти должна быть достаточной, чтобы иметь возможность легко применять SSE или другие векторизованные инструкции без необходимости сильно изменять свои структуры данных в данный момент.
-
Преобразования могут выполняться только с помощью pixel_data
. Логика переключения должна быть установлена на фактическом типе данных pixel
или pixel_matrix
, а тип разделен и повторно применен до и после преобразования.
Резюме
- Даняматические данные будут инкапсулировать статический макет данных с прикрепленной аннотацией типа, а статический тип данных - это параметр шаблона, позволяющий использовать различные базовые союзы или типы данных.
- Используйте статический макет данных для преобразований.
- Разделение различных базовых статических макетов данных достаточно важно, чтобы оправдать различные типы.
- Матричная форма динамических данных и статических данных достаточно важна, чтобы экономить пространство памяти и обеспечивать оптимизацию векторных инструкций.
Вот очень редкий код, который я придумал: http://coliru.stacked-crooked.com/a/76f31a9dd669a2fa