简体   繁体   中英

In C++, how can one map between enum values and data types, so the types can be used in templates?

How can one do this, which is obviously impossible C++, in real C++?:

Type decodeUiEnum(UiEnum myEnum) { // impossible: cannot return a data type
     // one switch statement to rule them all
     switch(myEnum) {
          case USER_SELECTED_GREYSCALE: return GreyscalePixel;
          case USER_SELECTED_RGB: return RgbPixel;
          ...
     }
}

void doSomeGraphicsMagic1(UiEnum myEnum) {
     ...
     Foo<decodeUiEnum(myEnum)> a(...); // impossible: type not available at compile 
                                       // time
     ...
}

void doSomeGraphicsMagic2(UiEnum myEnum, int blah) {
     ...
     Bar<int, decodeUiEnum(myEnum)> b(...); // impossible
     ...
}

and the like, so you can just add new types to the top switch statement and not have to modify the other code below it, so long as that code is suitably generic of course? As otherwise, you would need a switch statement within each function to do the necessary type mapping into the templates, which is not as much maintainable code, and lots of duplication. So more generally - if this is approaching it the wrong way, how do we fulfill that intended property of the code?

That is, what I want to do is, in a function taking an enum as parameter, instantiate a template type where the template parameter depends on the enum, without having a switch-on-enum in every function.

Yes it is actually possible.

Trick is based on partial template specification, this approach used by std::get

For example:

    #include <iostream>
    // specify an enumeration we will use as type index and related data types
    enum class UiEnum {
        GRAY_SCALE,
        RGB_PIXEL
    };
    
    struct GreyscalePixel;
    struct RgbPixel;
    
    // make base template class
    template<UiEnum _EV>
    struct ui_enum_type {
    };
    
    // do partial type specification trick
    // insert typedefs with data type we need for each enumeration value
    template<>
    struct ui_enum_type<UiEnum::GRAY_SCALE> {
        typedef GreyscalePixel pixel_type;
    };
    
    template<>
    struct ui_enum_type<UiEnum::RGB_PIXEL> {
        typedef RgbPixel pixel_type;
    };
    
    
    // demo classes to demonstrate how trick is working at runtime
    
    template<typename T>
    struct demo_class {
    };
    
    template <>
    struct demo_class<GreyscalePixel> {
        demo_class()
        {
            std::cout << "GreyscalePixel" << std::endl;
        }
    };
    template <>
    struct demo_class<RgbPixel> {
        demo_class()
        {
            std::cout << "RgbPixel" << std::endl;
        }
    };
    
    // use swithc trick
    static void swich_trick(std::size_t runtimeValue)
    {
        switch( static_cast<UiEnum>(runtimeValue) ) {
        case UiEnum::GRAY_SCALE: {
            demo_class< ui_enum_type<UiEnum::GRAY_SCALE>::pixel_type > demo1;
        }
        break;
        case UiEnum::RGB_PIXEL: {
            demo_class< ui_enum_type<UiEnum::RGB_PIXEL>::pixel_type > demo2;
        }
        break;
        }
    }
    
    int main(int argc, const char** argv)
    {
        // Do runtime based on the trick, use enum instead of data type
        for(std::size_t i=0; i < 2; i++) {
            swich_trick(i);
        }
        return 0;
    } 

In any case my suggestion - use classic polymorphism instead of template meta-programming over complication. Most modern compilers doing de-virtualization during optimization. For example:

#include <iostream>
#include <memory>
#include <unordered_map>

enum class UiEnum {
    GRAY_SCALE,
    RGB_PIXEL
};

class GraphicsMagic {
    GraphicsMagic(const GraphicsMagic&) = delete;
    GraphicsMagic& operator=(const GraphicsMagic&) = delete;
protected:
    GraphicsMagic() = default;
public:
    virtual ~GraphicsMagic( ) = default;
    virtual void doSome() = 0;
};

class GreyscaleGraphicsMagic final: public GraphicsMagic {
public:
    GreyscaleGraphicsMagic():
        GraphicsMagic()
    {
    }

    virtual void doSome() override
    {
        std::cout << "GreyscalePixel" << std::endl;
    }
};

class RgbGraphicsMagic final: public GraphicsMagic {
public:
    RgbGraphicsMagic():
        GraphicsMagic()
    {
    }
    virtual void doSome() override
    {
        std::cout << "RgbPixel" << std::endl;
    }
};

int main(int argc, const char** argv)
{
    std::unordered_map< UiEnum, std::shared_ptr< GraphicsMagic > > handlers;
    handlers.emplace(UiEnum::GRAY_SCALE, new GreyscaleGraphicsMagic() ) ;
    handlers.emplace(UiEnum::RGB_PIXEL, new RgbGraphicsMagic() );

    for(std::size_t i=0; i < 2; i++) {
        handlers.at( static_cast<UiEnum>(i) )->doSome();
    }

    return 0;
}

You could use std::variant , and then have consuming code std::visit that variant.

First we want a template for "pass a type as a parameter"

template <typename T>
struct tag {
    using type = T;
};

Then we define our variant and the factory for it.

using PixelType = std::variant<tag<GreyscalePixel>, tag<RgbPixel>>;

PixelType decodeUiEnum(UiEnum myEnum) {
    switch(myEnum) {
        case USER_SELECTED_GREYSCALE: return tag<GreyscalePixel>{};
        case USER_SELECTED_RGB: return tag<RgbPixel>{};
        ...
    }
}

Now our methods can be written as visitors over PixelType

void doSomeGraphicsMagic1(UiEnum myEnum) {
     std::visit([](auto t){
         using Pixel = decltype(t)::type;
         Foo<Pixel> a(...);
     }, decodeUiEnum(myEnum));
}

int doSomeGraphicsMagic2(UiEnum myEnum, int blah) {
     return std::visit([blah](auto t){
         using Pixel = decltype(t)::type;
         Bar<int, Pixel> a(...);
         return a.frob();
     }, decodeUiEnum(myEnum));
}

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