要模板还是不模板? (图片类)

时间:2016-09-18 15:29:49

标签: c++ templates

我有以下用例。

我需要创建一个Image类。图像定义如下:

  • 像素数(宽*高),
  • 像素类型(char,short,float,double)
  • 通道数量(单通道,3通道(RGB),4通道(RGBA)

上述类型的所有组合都是可能的。

此外,

  • 我有一些算法可以对这些图像进行操作。这些算法使用像素类型的模板。
  • 我需要与完全通用的文件格式(例如TIFF)进行交互。在这些文件格式中,像素数据保存为二进制流。

我的问题如下:我应该使用模板Image类还是通用接口?例如:

// 'Generic' Image interface
class Image {
  ...
  protected:
    // Totally generic data container
    uint8_t* data;
};


// Template Image interface
template <typename PixelType>
class Image {
  ...
  protected:
    // Template data container
    PixelType* data;
};

使用模板Image

我现在的问题是,如果我使用模板Image类,我的文件输入/输出将会很混乱,因为当我打开图像文件时,我不知道先验的是什么图像类型将是,因此我不知道要返回的模板类型。

这可能是最佳解决方案,如果我能想出一种创建通用函数的方法,该函数将从文件读取图像并返回通用对象,类似于

ImageType load(const char* filename);

但由于ImageType必须是模板,我不知道如何以及是否可以这样做。

使用通用Image

但是,如果我使用泛型Image类,我的所有算法都需要一个包含if / switch语句的包装函数,如:

Image applyAlgorithmWrapper(const Image& source, Arguments args) { 

  if (source.channels() == 1) {

    if      (source.type() == IMAGE_TYPE_UCHAR) {
      return FilterFunction<unsigned char>(source, args);
    } 
    else if (source.type() == IMAGE_TYPE_FLOAT) {
      return FilterFunction<float>(source, args);
    } else if ...

  } else if (source.channels() == 3) {

    if      (source.type() == IMAGE_TYPE_UCHAR) {
      return FilterFunction<Vec3b>(source, args);
    }
    ...
  }

(注意:Vec3b是一个通用的3字节结构,如

struct Vec3b {
  char r, g, b; 
};

3 个答案:

答案 0 :(得分:1)

在我看来,模板化的课程是首选的解决方案。

它将为您提供模板的所有优势,这基本上意味着您的代码库更清晰,更易于理解和维护。

使用模板化课程时,你所说的是一个问题并不是什么大问题。当用户想要阅读图像时,他/她应该知道他/她想要接收图像文件输出的数据类型。因此,用户应该这样做:

Image<float>* img;
LoadFromTIFF(*img, <filename>);

这与ITK等库中的方式非常相似。在您可能编写的模块中,您可以编写以从TIFF模块读取,您将执行此类型转换以确保返回用户声明的类型。

手动创建图像时,用户应执行以下操作:

Image<float>*img;
img->SetSize(<width>, <height>);
img->SetChannels(<enum_channel_type>);

从长远来看,这比使用非模板类更简单。

您可以查看ITK的源代码,了解如何在最一般意义上实现这一点,因为ITK是一个高度模板化的库。

编辑(附录) 如果您不希望用户对图像数据类型进行先验控制,则应考虑在TIFF标头中使用SMinSampleValue和SMaxSampleValue标记。这些标题存在于任何现代TIFF文件(版本6.0)中。它们旨在具有与TIFF文件中的示例数据类型匹配的TYPE。我相信会解决你的问题

答案 1 :(得分:1)

为了做出关于模板与非模板的正确决策(基于事实而不是意见),我的策略是衡量比较两个解决方案(模板和非模板)。我想衡量以下指标:

  • 代码行数
  • 性能
  • 编译时间

以及其他更主观的措施,例如:

  • 易于维护
  • 新生理解代码需要多长时间

我开发了一个非常大的软件[1],基于这些测量,我的图像类不是模板。我知道其他成像库提供两种选择[2](但我不知道他们有什么机制/代码是否仍然非常清晰)。我还有一些使用各种维度点(2d,3d,... nd)运算的算法,并且对于这些算法使算法成为模板导致性能增益使其值得。

简而言之,要做出正确的决定,有明确的标准,明确的衡量方法,并在玩具示例中尝试两种选择。

[1] http://alice.loria.fr/software/graphite/doc/html/

[2] http://opencv.org/

答案 2 :(得分:1)

模板。还有一个变种。如果你还没有C ++ 14,还有一个'界面助手'。让我解释一下。

每当您对给定操作有一组有限的特化时,您可以将它们建模为满足接口或概念的类。如果这些可以表示为一个模板类,那么这样做。它可以帮助您的用户只需要一个特定的专业化,当您从非类型化的源(例如文件)中读取时,您所需要的只是一个工厂。请注意,您还需要一个工厂 ,只是通常可以很好地定义返回类型。这就是我们来的地方......

变体。每当您不知道返回类型,但是您知道在编译时可能的返回类型集时,请使用变量。键入你的变体,使它看起来像'基类(请注意,没有涉及继承或虚函数),然后使用访问者。在C ++ 14中编写访问者的一种特别简单的方法是通用lambda,它通过引用捕获所有内容。实质上,从代码中的那一点开始,您就拥有了特定的类型。因此,将特定/模板化的类作为函数参数。

现在,boost::variant<>(或std::variant<>,如果有的话)不能拥有成员函数。要么驻留在'C-API样式'泛型函数(可能只是委托给成员函数)对称运算符;或者您有一个从您的变体类型创建的辅助类。如果你的CR允许它,你可能会从变体中下载 - 注意,有些人认为这种可怕的风格,其他人接受它作为图书馆作者的意图(因为,如果作者想要禁止继承,他们写了final)。 / p>

代码草图,不要尝试编译:

enum PixelFormatEnum { eUChar, eVec3d, eDouble };

template<PixelFormatEnum>
struct PixelFormat;

template<>
struct PixelFormat<eUChar>
{
    typedef unsigned char type;
};
// ...

template<PixelFormatEnum pf>
using PixelFormat_t = typename PixelFormat<pf>::type;

template<PixelFormatEnum pf>
struct Image
{
    std::vector<std::vector<PixelFormat_t<pf> > > pixels; // or anything like that
    // ...
};

typedef boost::variant< Image<eUChar>, Image<eVec3d>, Image<eDouble> > ImageVariant;

template<typename F>
struct WithImageV : boost::static_visitor<void>
{
    // you could do this better, e.g. with compose(f, bsv<void>), but...
    F f_;

    template<PixelFormatEnum e>
    void operator()(const Image<e>& img)
    {
        f_(img);
    }
}

template<typename F>
void WithImage(const ImageVariant& imgv, F&& f)
{
    WithImageV v{f};
    boost::apply_visitor(v, img);
}

std::experimental::optional<ImageVariant> ImageFactory(std::istream& is)
{
    switch (read_pixel_format(is))
    {
    case eUChar: return Image<eUchar>(is);
    // ...
    default: return std::experimental::nullopt;
    }
}

struct MyFavoritePixelOp : public boost::static_visitor<int>
{
    template<PixelFormatEnum e>
    int operator()(PixelFormat_t<e> pixel) { return pixel; }

    template<>
    int operator()(PixelFormat_t<eVec3d> pixel) { return pixel.r + pixel.g + pixel.b; }
};

int f_for_variant(const ImageVariant& imgv)
{
    // this is slooooow. Use it only if you have to, e.g., for loading.
    // Move the apply_visitor out of the loop whenever you can (here you could).
    int sum = 0;
    for (auto&& row : imgv.pixels)
       for (auto&& pixel : row)
           sum += boost::apply_visitor(MyFavoritePixelOp(), pixel);
    return sum;
}

template<PixelTypeEnum e>
int f_for_type(const Image<e>& img)
{
    // this is faster
    int sum = 0;
    for (auto&& row : img)
       for (auto&& pixel : row)
           sum += MyFavoritePixelOp()(pixel);
    return sum;        
}

int main() {
    // ...
    if (auto imgvOpt = ImageFactory(is))
    {
        // 1 - variant
        int res = f_for_variant(*imgvOpt);
        std::cout << res;

        // 2 - template
        WithImage(*imgvOpt, [&](auto&& img) {
           int res2 = f_for_type(img);
           std::cout << res2;
        });
    }
}