简体   繁体   中英

Passing struct parameter in template method c++

I have a problem with passing parameter to class template

struct Car {
    int id;
    char *model;
    int date;
    int cost;
};

template <class T>
class Set
{
private:
    int *a;
    int _size;
    map<int, T> data;
public:

    //some methods before

    void insert(T x)
    {

        int num;
        if (std::is_same<T, Car>::value)
            num = x.id;
        if (num >= 0 && num < (_size << 5))
            throw "Element is out of set size!";
        a[num / 32] = a[num / 32] | (1 << (num % 32));
        if (std::is_same<T, Car>::value)
            data.insert(make_pair(num, x));
    }
};

My insert method should accept both int type and Car struct. But Visual Studio says that I have compilation error at num = x.id; that x should be a class, struct or union. I could pass pointers to this function, but I won't be able to pass integers like class.insert(5) . How can I solve it or how I can make method to accept both pointers to struct and regular variables without making another specification to one of the types?

Yes, this is a problem a lot of people struggle with. Let's look at your template method snipped:

void insert(T x)
{
    int num;
    if (std::is_same<T, Car>::value)
        num = x.id;
    //...

The problem here is that if statement semantically evaluated at run time (even if compiler can optimize the branch away because it will know the value at compile time) and thus, compiler needs to 'pretend' to generate code for both branches. On a side note, when branch is not taken, your num remains unitialized, which is undefined behavior.

In your case it means that even when T is not Car , semantically code still needs to be generated for num = x.id . Obviously, int.id is not a valid statement.

To solve this problem with C++17 you would use if constexpr :

if constexpr (std::is_same<T, Car>::value) //...

Constexpr if s are guaranteed to be evaluated at compile time, and code belonging to not taken branch would not be evaluated.

In the pre-C++17 world, you usually can use either tag dispatch or SFINAE. I always prefer tag dispatch, as I think it is way more straightforward than SFINAE. Tag dispatch relies on using function overload, and in your case would be something like (since it's unclear what your code would do when passed int , I will pretend you need to set num to int passed:

int get_id(const Car& car) {
    return car.id;
}

int get_id(int v) {
    return v;
}

void insert(const T& t) {
    int num = get_id(t);
    // ...

Please note, in this particular case it is not even tag dispatch, as the case is very simple.

Consider this simplified example:

template <typename T> 
struct foo { 
    T t;
    void bar() {
        if (std::is_same<T,Car>) { std::cout << t.id; }
    }
};

Now you have to remember that template instantiation happens at compile time, ie for foo<Car> the compiler creates a

 struct foo<Car> {  
     Car t;
     void bar() {
          if (true) { std::cout << t.id; }
     }
 };

so far so good, but when you instantiate the same template for int you get

 struct foo<int> {  
     int t;
     void bar() {
          if (false) { std::cout << t.id; }
     }
 };

the problem is that even though the condition is always false, the code still needs to compile. int has no id , hence your code fails to compile.

There are several ways to solve your problem. You could use SFINAE (which can be a bit confusing if you are not used to it, like me ;) , if constexpr (which avoids that not taken branches must be valid) or simply explicitly provide the specializations you need.

To make that line even compile you'd have to use if constexpr :

        if constexpr(std::is_same<T, Car>::value) num = x.id;

But note that as of your current code, otherwise num remains uninitialized (and used in the following line nevertheless).

The most reasonable way, if you want to be able to insert arguments of types T , Car and int , is probably to spread the functionality between the three overloads:

void insert(T x);
void insert(Car x);
void insert(int x);

And additionally specialize the template for both types Car and int , otherwise it won't compile.

Or did you mean that the Set should actually accept elements of all types? Then it is a much trickier type. First of all, the insert method itself should be templated:

template<typename T> void insert(T x);

while the class itself, most likely, should not:

class Set;

(Then in the templated insert you can use everything that has been said about if constexpr in this thread.)

Next, the underlying container should accept values of all types, which means you'll need to use something like map<int, std::any> (BTW, why map ? Order-by-ids is not probably very consistent if you store cars intermixed with integers. You can use unordered_map here, or even unordered_set , with your own multityped hash function.)

And last but not the least, it will take a while to craft a minimally consistent scheme to actually retrieve stored values. The major problem here, is you'll have to store them type-erased, so you'll need to somehow recall the actual type of a stored value — in a not-totally-unelegant way.

Is that what you're trying to build?

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