5

First of all, I would like to say this is the first question I am asking on stackOverflow, so I apologize if I am not clear enough.

My question regards referring parametrically to a struct feature inside a function. I work in C++.

What I really want to achieve is to be able to sort a vector of struct objects (or class objects) based on a specific struct feature, which is given as a parameter. I also want to give the type of struct through a template, so some workarounds that deal with specific situations might not work in general.

I will show a simplistic example of what I mean.

Let's say, I have a struct called "human" with features: "age", "height", "weight".

Let's also assume that I have a vector of "human" objects called "mankind".

Here, let's say I want to make a function that can output to the screen each element's age, height or weight, depending on what I pass as a parameter.

The code below obviously doesn't work. I am asking for the proper way to do this.

struct human{
    int age;
    int height;
    int weight;
};

void show(vector<human> &elements, int value){
    for (int i=0; i<elements.size(); i++)
        cout << elements[i].value << endl;
}

int main{
    ...
    vector<human> mankind;
    ...
    show(mankind, age);
    show(mankind, height);
    show(mankind, weight);
    ...
    return 0;
}

I want to point out, that this example is a very simple case. Of course, I can make this work if I make separate functions for each feature or if I use a cheeky way, like passing a string "age" or "height" or "weight" as a parameter, checking it inside the function and having a completely separate case for each one.

However, such workarounds won't work in the general case of the problem, especially if I have many different types of structs (passed through a template T and vector< T > ) and features.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
New Guy
  • 51
  • 1

3 Answers3

4

One way to do this is to use pointers-to-members, a C++ feature that lets you create pointers that refer to specific fields of a struct or class.

The syntax here might look a little scary, but it's actually not so bad. For example, here's how you'd get a pointer to the height field of your human struct:

int human::* ptr = &human::height;

Here, the syntax int human::* ptr means

  1. that it's a pointer to something inside the human type;
  2. specifically, it's a pointer to an int data member; and
  3. that data member is the height field.

Once you have this pointer, you can combine it with a human struct to isolate out just that member like this:

human agatha;
cout << agatha.*ptr << endl;

Here, the .* operator means "pull up the field pointed at by ptr.

In your case, you might put things together like this:

void show(vector<human> &elements, int human::* value){
    for (int i=0; i<elements.size(); i++)
        cout << elements[i].*value << endl;
}

int main{
    ...
    vector<human> mankind;
    ...
    show(mankind, &human::age);
    show(mankind, &human::height);
    show(mankind, &human::weight);
    ...
    return 0;
}
templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
3

I prefer the lambda approach to this problem. If the function doesn't know what will be printed, just the calling code, then you can pass a lambda to the function that contains the code of what to print. That way you can make the function completely generic like

template <typename T, typename Func>
void show(vector<T> const& elements, Func f){
    for (auto const& e : elements)
        cout << f(e)<< endl;
}

and then you would call it like

show(mankind, [](auto const& e){return e.age;});
show(mankind, [](auto const& e){return e.height;});
show(mankind, [](auto const& e){return e.weight;});

If show needs to show this member in order then you can even leverage the lambda in you call to std::sort like

template <typename T, typename Func>
void show(vector<T>& elements, Func f){
    std::sort(elements.begin(), elements.end(), [=](auto const& lhs, auto const& rhs){return f(lhs) < f(rhs);});
    for (auto const& e : elements)
        cout << f(e)<< endl;
}

So the element to print is used to the sort vector and to print the element.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
2

Here are some thoughts on and improvements to the other answers:

1) If your struct contains members of different types, you'll have to overload all functions using the pointer to member for each of the types. Or you'd have to use a template like:

#include <vector>
#include <iostream>

struct human{
    int age;
    int height;
    float weight;
};

template<typename T>
void show(std::vector<human> &elements,  T human::* value){
    for (int i=0; i<elements.size(); i++)
        std::cout << elements[i].*value << std::endl;
} 

2) I find the lambda approach more flexible, as it allows you to use combined features which could prove useful in real-world applications:

// body-mass-index
auto bmi = [](const human& e) { return e.height / (e.weight * e.weight); };

show(mankind, bmi);

3) Also, the lambda approach allows you to manipulate features for sorting, filtering etc:

auto inverse = [](auto func) {
    return [=](const human& e) { return -func(e);};
};

template<typename T, typename Func>
void sort(std::vector<T> &elements, Func f) {
    std::sort(elements.begin(), elements.end(), [=](auto const &lhs, auto const &rhs) { return f(lhs) < f(rhs); });
}

sort(mankind, inverse(bmi));

4) The lambda approach allows you to use you exactly the specified syntax on the call site, if you put the lambdas into global variables (see the bmi example above).

5) Starting with C++14 we have generic lambdas, so you can reuse the lambdas for multiple different types of structs.

florestan
  • 4,405
  • 2
  • 14
  • 28