简体   繁体   中英

SFINAE + template specialization

I need an image class template which will work with images with different number of dimensions and different format. And I also want to define different class template specializations for 1D, 2D and 3D images (only those three, others will be left undefined - so that they cannot be used).

Abstract code:

enum image_format {R, G, B, RG, GB, RB, RGB}; // <--- Image format enumeration.
template<int dimensions, image_format format> // <--- Primary image class template.
class image;                                  // Only few dimensions will be defined.

template<image_format format> // <--- Partial template specialization used here
class image<2, format>        // to define image class for 2D images only.
{
private:
    /* Class data here */
public:
    image(int size_x, int size_y, char * bytes); // <--- 2D image constructor initializes
                                                 // object with given size and data.
};

template<image_format format, typename = typename std::enable_if // <--- SFINAE used here with unnamed template
        <format == R or format == G or format == B>::type>       // argument and accepts only R or G or B.
image<2, format>::image(int size_x, int size_y, char * bytes)
{
    /* Code for R- or G- or B-formatted images only */
}
template<image_format format, typename = typename std::enable_if
        <format == RG or format == GB or format == RB>::type>
image<2, format>::image(int size_x, int size_y, char * bytes)
{
    /* Code for RG- or GB- or RB-formatted images only */
}
template<image_format format, typename = typename std::enable_if
        <format == RGB>::type>
image<2, format>::image(int size_x, int size_y, char * bytes)
{
    /* Code for RGB-formatted images only */
}

I could use template specialization on constructors to use different code for each format, but constructors need to accept multiple template arguments. In my case first constructor would be invoked with single-letter-formatted images (ie image<2, R> or image<2, G> or image<2, B> ), the second one with two-letter-formatted images and so on. I would need to write same code in each template specialization if I were to separate R, G and B constructors, so I wonder if I could use SFINAE or anything else here to write the code only once.

When I try to compile this code, I get two redefinition of image<2, format>::image (which comes from multiple constructor realizations I guess) and / or too many template parameters in template redeclaration (which comes from extra parameter in constructor realization template, the parameter used for SFINAE). Seems that I am not that far from achieving what I want, but something is still wrong and I can't figure out what.

I know that static_assert or template argument checking will suffice here, but I want to know if I can do what I want without these, only using template specialization and SFINAE.

This is a bit easier than you think... but first, you are using std::enable_if wrong: if you do not supply a type then it defaults to void , meaning all your methods resolve to the same type . Turn your compiler's warnings up and it will tell you this kind of stuff.

Here is one way to do what you want, complete with test program:

// cl /EHsc /W4 /Ox /std:c++17 /wd4100 a.cpp
// clang++ -Wall -Wextra -Werror -pedantic-errors -O3 -std=c++17 -Wno-unused-parameter a.cpp

#include <iostream>
#include <type_traits>

enum image_format {R, G, B, RG, GB, RB, RGB}; // <--- Image format enumeration.

template<int Dimensions, image_format Format>
struct image
{
  image(int size_x, int size_y, char * bytes);

private:
  // Here is the common code for the image<2,R|G|B> constructor to use.
  void image2R(int size_x, int size_y, char * bytes)
  {
    std::cout << "R or G or B\n";
  }

  // (Repeat for image<2,RG|RB|GB>)
  // void image2RG(int size_x, int size_y, char * bytes)
  // {
  //   ...
  // }
};


// Here our constructors will simply dispatch to the common code
template<> image<2, R>::image(int size_x, int size_y, char * bytes) { image2R(size_x, size_y, bytes); }
template<> image<2, G>::image(int size_x, int size_y, char * bytes) { image2R(size_x, size_y, bytes); }
template<> image<2, B>::image(int size_x, int size_y, char * bytes) { image2R(size_x, size_y, bytes); }
// template <> void image<2, RG>::image2RG(int, int, char *) = delete;
// ...


// (Repeat for image<2, RG>, etc.)


// The RGB method only has one variant, so no need for a common code method.
template<> image<2, RGB>::image(int size_x, int size_y, char * bytes)
{
  std::cout << "RGB\n";
}
template<> void image<2, RGB>::image2R(int, int, char *) = delete;
// template<> void image<2, RGB>::image2R(int, int, char *) = delete;
// ...


int main()
{
  image<2, RGB> imgRGB(10, 10, nullptr);
  image<2, G>   imgG  (54, -7, nullptr);
  
//  image<3, RGB> fooey (11, 11, nullptr);  // link error: unresolved external symbol
//  image<2, RG>  quuxl (17, 17, nullptr);  // link error: unresolved external symbol

//  imgRGB.image2R(9,8,nullptr);  // attempting to use a deleted function
//  (If you do uncomment the above line, make sure to
//   comment out the "private:" in the class definition above)

  std::cout << "Hello world!\n";
}

This works because:

  • We do not define the constructor for undesired template combinations, so that your users cannot instantiate them.
  • We put common construction code into a private “helper” function, and dispatch to it from the appropriate constructors. Write a constructor helper for each valid commonality.
  • We = delete the common helper functions that do not belong to a given template combination, so that they are not part of that class.

This isn't the only way to accomplish your goal. There are other ways to do it. Personally, I'm not sure your class design is something I would accept... but I don't know your requirements or how much control you have over them.

For additional reading, you might want to Google around “SFINAE” and also “C++ tag dispatch”.

You cannot use SFINAE only in definition. Declaration and definition signatures should match. As default argument is not part of signature, instead of

template <typename T, typename = std::enable_if_t<cond<T>>>

use

template <typename T, std::enable_if_t<cond<T>, int> = 0>

In C++17, if constexpr might help:

template<image_format format> // <--- Partial template specialization used here
class image<2, format>        // to define image class for 2D images only.
{
private:
    /* Class data here */
public:
    image(int size_x, int size_y, char * bytes)
    {
        if constexpr (format == image_format::R
                    || format == image_format::G
                    || format == image_format::B) {
            // ...
        } else if constexpr (format == image_format::RG) {
            // ...
        } // ...
    }
};

Alternative to SFINAE in C++20 is constraint with requires :

template<image_format format> // <--- Partial template specialization used here
class image<2, format>        // to define image class for 2D images only.
                              // Note that requires might be also used to avoid specialization
{
private:
    /* Class data here */
public:
    image(int size_x, int size_y, char* bytes)
    requires(format == image_format::R
          || format == image_format::G
          || format == image_format::B)
    {
        // ...
    }
    image(int size_x, int size_y, char* bytes)
    requires(format == image_format::RG
          || format == image_format::GB
          || format == image_format::RB)
    {
        // ...
    }

    // ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM