以在编译时和运行时都可用的方式描述C++像素格式信息

Describing pixel format information in C++ in a way that is usable at both compile-time and runtime

本文关键字:描述 方式 C++ 像素 信息 格式 编译 运行时      更新时间:2023-10-16

我有一个对像素进行操作的库。像素可以采用许多不同的格式。我正在寻找一种有效的方法来描述库 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详细信息时,复制和存储像素类型的详细信息对我来说似乎很愚蠢。从概念上讲,程序所需要的只是对特定pixel_type的 ID/地址/引用,以及在编译时和运行时检索相关属性(颜色空间、通道等)的方法。

此外,如果我想

从像素类型获取派生属性,如果我想避免重复逻辑,我需要在pixel_layout上实现它。然后要在编译时使用它,我需要从类到pixel_layout实例再到派生属性pixel_type<...>。这也显得有点傻。

我是否可以避免传递pixel_layout详细信息,而是使用对pixel_type<...>(子)类的某种引用?

我尝试使用enum s,因为枚举用作模板参数和函数参数。但我很难从枚举值中获取(例如 rgba )到像素类型属性(例如4个通道)在运行时和编译时以惯用C++方式。

此外,枚举作为模板参数在编译错误期间提供的有用诊断要少得多。例如,我在编译带有 clang 的错误消息中得到image<(pixel_type)2>而不是image<rgba>。因此,这似乎不是一个有用的方法。

使用非类型引用模板参数可能是一种解决方案。请参阅 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's 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&

但它们仍然可以在运行时使用。

我会首先将rgbrgba等设置为空类:

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中介。

我可能缺少一些用例,因此您可能需要对其进行一些调整,但我希望它有所帮助。

回答你的问题:

我是否可以避免传递pixel_layout详细信息?,而是使用对pixel_type<...>(子)类的某种引用?

是的,您可以使用继承。您希望在运行时表达一个通用接口,因此至少需要一个数据成员指出差异,如前所述。界面如下所示:

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();的东西而不会泄漏性能。让我们解决之前尚未解决的问题。我们正在为每个指向同一 vtable 的每个pixel_layout制作新的concept_implementation对象。我们可以通过在多个实例上为每个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(); }
};

现在我们只需要指向我们pixel_layout中的shared_instance

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

这是一个尝试。也许我错过了一些东西,但似乎你可以在没有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>;

我发现使用别名可以轻松破译编译器消息(尤其是使用 clang),这就是我选择在这里使用它们的原因。

现在我只能看到layout函数正在做的是将您已经作为模板参数(color_spaceuint8_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
};

但是,您可以通过使 this 转换类成为模板类来执行此操作(非常好),并将 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

但我认为这更像是一个可读性问题,而不是影响功能的问题。

简单演示

我想出的解决方案涉及三种数据结构:

  • 类型批注枚举enum class pixel_type : char { };
  • 包含像素数据的联合类型union pixel_data { };
  • 用于保存两者的结构,以及用于指定默认类型注释的模板参数:struct pixel{ };

编译时/静态:

  • 如果您已经知道格式类型,请使用模板参数建立格式类型。
  • 作为一个工会,pixel_data将在某种程度上"无单位"。如果您已经知道类型,则可以编写函数来假设数据采用正确的表示形式。如果您需要可变量的内存来容纳不同的像素数据格式,这显然是一个值得明确解决的问题。如果是这样,请修改 pixel 类以参数化基于像素内存表示形式大小偏离的内部实现。
  • 在运行时对需要切换类型的数据使用 pixel 类。完成后,可以仅使用类型注释的成员pixel_data解包类型批注,然后对其执行转换。通过将实际的基础数据类型和元数据分开,应该可以更轻松地对大量数据执行数据计算,而不必跳过仅属于该类的成员函数,如果您决定使用多线程,这可能会变得很麻烦。实际上,我想说的是,如果您担心这一点,您也可以在像素类中包含锁定,甚至可以将其应用于基础数据。这取决于你。
  • 使用 pixel_data 在可以假定类型的代码字段中进行计算;否则,将 pixel 类视为一种变体或可选数据类型。

运行时/动态

  • 使用pixel_data::unknown确保运行时的安全。如果您愿意,您可以创建一种apply函数,该函数将充当 monadic 保护,以防止类似NULL格式通过。
  • 确定格式后,在
  • pixel结构中设置格式。
  • 同样,您仍然可以删除pixel数据包装器,并在计算过程中根据需要丢弃类型注释。

海量像素存储和计算/转换

  • 创建一种带有类型注释的容器类;这是为了保存许多具有相同数据类型的数据类型;为每个像素浪费一个字节来保存像素格式是浪费的;如果你想存储一个矩阵,只需为整个矩阵存储一个字节,并再次参数化实际的基础数据类型。这应该是某种pixel_matrix,底层内存结构应该足以轻松应用SSE或其他矢量化指令,而不必在这一点上大量修改数据结构。

  • 转换只能使用 pixel_data 完成。切换逻辑应针对实际pixelpixel_matrix数据类型进行,并在转换前后剥离并重新应用类型。

总结

  • Dyanmic 数据将封装静态数据布局,并附加类型注释,静态数据类型是模板参数,以允许不同的底层联合或数据类型。
  • 使用静态数据布局进行转换。
  • 分离不同的底层静态数据布局非常重要,可以保证不同的类型。
  • 动态数据和静态数据的矩阵形式对于节省内存空间并允许矢量化指令优化非常重要。

这是我想出的一些非常稀疏的代码:http://coliru.stacked-crooked.com/a/76f31a9dd669a2fa