Описание информации о формате пикселей в С++ способом, который можно использовать как во время компиляции, так и во время выполнения

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