简体   繁体   中英

std::tuple, automatic constructor order

Consider the following code snippet

struct MachineGun {
    explicit  MachineGun (int a);
};

struct Turret {
    explicit  Turret (char b);
};

struct Cannon {
    explicit  Cannon (std::string c);
};


template<typename... Ts>
struct Enemy {
    template<typename...Args>
    Enemy(Args&&... args)
        : device_list {std::forward<Args>(args)...}
    {

    }

private:
    std::tuple<Ts...> device_list;
};

In the above constructor std::forward with the args unpack matches the correct constructor of the type inside the tuple but I cannot figure out how to try to make this match regardless of order of the types or parameters.

Hypothetically the code below represent the same thing: "Give me an enemy with a machinegun, turret and cannon"

Enemy<MachineGun,Turret,Cannon> e1(1,'1',std::string("1"));
Enemy<MachineGun,Turret,Cannon> e2("3",14,'5');
Enemy<Turret,MachineGun,Cannon> e3(std::string("14"), 5, '33');

But how do I actually enable this? e1 will compile as the args list matches the type list order, both e2 and e3 should represent the same thing but how do I make my compiler understand this?

The biggest problem that I see in your question is the risk that the same value can initialize two different object in your tuple.

By example, given

Enemy<MachineGun,Turret,Cannon> e2("3",14,'5');

the integer ( 14 ) can initialize both MachineGun (a int constructor) and Turret (a char constructor and implicit conversion from int to char ).

Same problem for '5' : can initialize both MachineGun (a int constructor and implicit conversion from char to int ) and Turret (a char constructor).

The only way I see to solve this problem is to avoid constructor collisions adding deleted constructors.

So you can add a deleted char constructor for MachineGun

struct MachineGun
 {
   explicit MachineGun (int i)
    { /* something */ }

   MachineGun (char) = delete;
 };

and a deleted int constructor for Turret

struct Turret
 {
   explicit Turret (char c) 
    { /* something */ }

   Turret (int) = delete;
 };

More generally speaking, you have to add all deleted constructor necessary to avoid all possible ambiguities and to enable only one constructor for every argument you pass to Enemy constructor.

Anyway, given this simplification, you can write a couple of SFINAE alternative functions to select the right element, from a list, to construct an object

template <typename T, typename A0, typename ... As>
std::enable_if_t<true == std::is_constructible<T, A0>::value, T>
    selectArg (A0 && a0, As ...)
 { return T{ std::forward<A0>(a0) }; }

template <typename T, typename A0, typename ... As>
std::enable_if_t<false == std::is_constructible<T, A0>::value, T>
    selectArg (A0, As && ... as)
 { return selectArg<T>(std::forward<As>(as)...); }

And Enemy simply become

template <typename ... Ts>
struct Enemy
 {
   private:
      std::tuple<Ts...> device_list;

   public:
      template <typename ... Args>
      Enemy (Args ... args)
        : device_list { selectArg<Ts>(args...)... }
       { }
 };

The following is a full working example

#include <tuple>
#include <iostream>
#include <type_traits>

struct MachineGun
 {
   explicit MachineGun (int i)
    { std::cout << "- MachineGun: " << i << std::endl; }

   MachineGun (char) = delete;
 };

struct Turret
 {
   explicit Turret (char c) 
    { std::cout << "- Turret: " << c << std::endl; }

   Turret (int) = delete;
 };

struct Cannon
 {
   explicit Cannon (std::string const & s)
    { std::cout << "- Cannon: " << s << std::endl; }
 };

template <typename T, typename A0, typename ... As>
std::enable_if_t<true == std::is_constructible<T, A0>::value, T>
    selectArg (A0 && a0, As ...)
 { return T{ std::forward<A0>(a0) }; }

template <typename T, typename A0, typename ... As>
std::enable_if_t<false == std::is_constructible<T, A0>::value, T>
    selectArg (A0, As && ... as)
 { return selectArg<T>(std::forward<As>(as)...); }

template <typename ... Ts>
struct Enemy
 {
   private:
      std::tuple<Ts...> device_list;

   public:
      template <typename ... Args>
      Enemy (Args ... args)
        : device_list { selectArg<Ts>(args...)... }
       { }
 };

int main()
 {
   Enemy<MachineGun,Turret,Cannon> e1(1,'2',std::string("3"));
   Enemy<MachineGun,Turret,Cannon> e2("3",14,'5');
   Enemy<Turret,MachineGun,Cannon> e3(std::string("14"), 5, '3');
 }

Using typename std::enable_if<...>::type instead of std::enable_if_t<...> the solution works also with C++11.

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