I would like to take advantage of C++ templates for code reuse and type safety, but I keep finding myself at the API edges doing some rather clunky stuff to transition between a template-based implementation and external interfaces which are runtime-data-based. I'm wondering if there's some way to get the compiler to help with some of the transition (i.e. do some of this work for me.)

Let's consider a contrived case where we want to do some operation on an image, like convert it to another color space. Say we have some arbitrary image class:

struct Image { /* Whatever */ };

Then we have an enum of the kinds of conversions we support:

enum class ImageType : uint8_t {
    RGB,
    CMYK,
    Grayscale
};

Then we've got some private implementation that is templated to reuse code, etc:

// Internal implementation
template <ImageType T>
struct ImageConverter {
public:
    Image ConvertImage(const Image& img);
private:
    void some_shared_code(Image& img) {
        // do stuff...
    };
};

Then we've got some method instantiations for each type. (Note that to the caller, these all share the same return type and parameter list).

template <> Image ImageConverter<ImageType::RGB>::ConvertImage(const Image& img)
{
    Image foo = img;
    some_shared_code(foo);
    // do other stuff specific to this color space...
    return foo;
};

template <> Image ImageConverter<ImageType::CMYK>::ConvertImage(const Image& img)
{
    Image foo = img;
    some_shared_code(foo);
    // do other stuff specific to this color space...
    return foo;
};

template <> Image ImageConverter<ImageType::Grayscale>::ConvertImage(const Image& img)
{
    Image foo = img;
    some_shared_code(foo);
    // do other stuff specific to this color space...
    return foo;
};

And then finally, we want to vend this to the outside world as a non-templated API, like this:

Image ConvertImage(const Image& inImage, ImageType toType) {
    switch (toType) {
        case ImageType::RGB: {
            ImageConverter<ImageType::RGB> ic;
            return ic.ConvertImage(inImage);
        }
        case ImageType::CMYK: {
            ImageConverter<ImageType::CMYK> ic;
            return ic.ConvertImage(inImage);
        }
        case ImageType::Grayscale: {
            ImageConverter<ImageType::Grayscale> ic;
            return ic.ConvertImage(inImage);
        }
    }
};

And it's this last part that bothers me -- it's ugly and clunky. For what it's worth, this is obviously a contrived example that uses non-type template parameters for brevity, but the problem exists in the abstract (i.e. when the template parameters are types.)

I'm aware of the pattern of declaring a pure-virtual "interface" class from which all your template instantiations then inherit from, but that requires that the template instantiations inherit from the interface class. When consuming third party classes, sometimes that isn't really an option. (It also has other drawbacks, such as changing the layout in memory, etc.)

Is there some idiom, working in that abstract space that can more elegantly fill the role of this switch statement but not require onerous changes to the implementations (such as inheriting from a non-templated interface class)? I feel like this must be a common problem and that there's probably some clever solution that's just beyond the reach of my current template-fu.

EDIT: The more I think about this the more I've started to think that the answer is probably template metaprogramming based (in as much as template metaprogramming is ever the answer to anything.)

有帮助吗?

解决方案 2

I came up with something that sort of works, but I think some template metaprogramming wizards are going to have to weigh in on whether there's a better way. Here's what I came up with:

First some "model"-ish stuff:

enum class ImageType : uint8_t {
    RGB,
    CMYK,
    Grayscale,
    Invalid
};

struct Image {
    Image(ImageType type) : img_type(type) {};

    ImageType img_type;
    // other stuff...
};

Then our templated converter class

template <ImageType T>
struct ImageConverter {
public:
    Image ConvertImage(const Image& img);
private:
    void some_shared_code(Image& img) {
        // do stuff...
    };
};

And the corresponding specialized versions of it:

template <> Image ImageConverter<ImageType::RGB>::ConvertImage(const Image& img)
{
    Image foo = img;
    foo.img_type = ImageType::RGB;
    some_shared_code(foo);
    return foo;
};

template <> Image ImageConverter<ImageType::CMYK>::ConvertImage(const Image& img)
{
    Image foo = img;
    foo.img_type = ImageType::CMYK;
    some_shared_code(foo);
    return foo;
};

template <> Image ImageConverter<ImageType::Grayscale>::ConvertImage(const Image& img)
{
    Image foo = img;
    some_shared_code(foo);
    foo.img_type = ImageType::Grayscale;
    return foo;
};

And now the fun part! Essentially I've used a variadic template to recursively search through a list of options provided at the call site.

template<ImageType T, ImageType... Args> struct _maker
{
    Image operator()(ImageType desiredType, const Image& inImage)
    {
        if (T == desiredType)
        {
            auto converter = ImageConverter<T>();
            return converter.ConvertImage(inImage);
        }
        else
        {
            return _maker<Args...>()(desiredType, inImage);
        }
    };
};

Once I had this recursive template thing going, I needed a way to stop at the bottom of the recursion. I'm not thrilled about having to have added Invalid to the model to have something to stop on, but it was straight forward enough.

template<> struct _maker<ImageType::Invalid>
{
    Image operator()(ImageType desiredType, const Image& inImage)
    {
        return Image(ImageType::Invalid);
    };
};

And then in the non-templated API, I use these recursive templates, with a list of the the various options, and pass it the runtime-data value to match against, and the input image.

Image ConvertImage(ImageType desiredType, const Image& inImage)
{
    return _maker<ImageType::RGB, ImageType::CMYK, ImageType::Grayscale, ImageType::Invalid>()(desiredType, inImage);
};

Called like this:

Image x = Image(ImageType::RGB);
Image y = ConvertImage(ImageType::Grayscale, x);

if (x.img_type == y.img_type)
{
    cout << "not converted\n";
}
else
{
    cout << "converted\n";
}

What we end up with, at the deepest point is a backtrace like this:

Backtrace

So this mostly achieved my goal of getting rid of the switch statement. It would be nice if it were a little cleaner (i.e. I didn't have to jump through hoops to stop at the bottom of the recursion) or if it weren't recursive. But this C++11 variadic template stuff was already kind of pressing my luck.

Hopefully some journeyman template metaprogrammer has a better idea.

其他提示

One of the possible solutions that comes to my mind is to use type tags instead of enum:

struct RGBConv{};
struct CMYKConv{};
struct GrayscaleConv{};  

Then you can declare ImageConverter like this:

struct ImageConverter {
public:
  template <typename ConvT>
  Image ConvertImage(const Image& img, ConvT conv);
private:
  void some_shared_code(Image& img) {
    // do stuff...
  };
};

And then specialize ConvertImage on each image converter type:

template <> Image ImageConverter::ConvertImage<RGBConv>(const Image& img, RGBConv conv){/*...*/}
template <> Image ImageConverter::ConvertImage<CMYKConv>(const Image& img, CMYKConv conv){/*...*/}
template <> Image ImageConverter::ConvertImage<GrayscaleConv>(const Image& img, GrayscaleConv conv){/*...*/}

Now we can get rid of switch statement:

template <typename T>
Image ConvertImage(const Image& inImage, T conv) {
  return ImageConverter().ConvertImage(inImage, conv);
};

Yes, ConvertImage is still templated function but thanks to ADL we can call it just as ordinary function like this:

ConvertImage(Image(), RGBConv());
ConvertImage(Image(), GrayscaleConv());
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top