3

I have a struct containing 19 variables and I need to use different sorting algorithms to sort this struct, the thing is that it can be sorted by any of these variables. I'm wondering if there is any way to dynamically access this data in the struct, that way I can just write one method for the sort instead of writing 19 different methods that run the same sorting algorithm but on the different variables inside.

So I have my struct

struct team_stats{
    float yards_per_game;
    int total_points;
etc etc

team_stats* arr = new team_stats[32];

I want to do something like this (obviously it wont be a string cause this doesn't make sense, but just the idea behind this):

quickSort(arr, "yards_per_game"); // sorts by yards per game
quickSort(arr, "total_points"); // sorts by total points

quickSort(team_stats* arr, string field) {
    while (i <= j) {
        while (arr[i].field < pivot)
etc. etc.

Instead of doing it like this:

if (field == "yards_per_game")
    quickSortYards(arr);
else if (field == "total_points")
    quickSortPoints(arr);

quickSortYards(team_stats* arr) {
    while (i <= j) {
        while (arr[i].yards_per_game < pivot)
etc. etc.

quickSortPoints(team_stats* arr) {
    while (i <= j) {
        while (arr[i].total_points < pivot)
etc. etc.

because the latter would require me to have to write 38 different functions for just for sorting with one algorithm, and I feel like that is just a mess

Thank you

3 Answers3

6

If all of your data fields had the same type, it would have been possible to implement using purely pointers-to-members and without using templates.

However, it looks like you have different types for different fields. In that case templates are the simplest way to go about it. For example, you can use something like this

template <typename T, typename M> void sort_by_field(T a[], size_t n, const M T::*p)
{
  std::sort(a, a + n, [p](const T &l, const T &r) { return l.*p < r.*p; });
}

struct S
{
  int a;
  float b;
};

int main()
{
  S s[100] = { ... };
  sort_by_field(s, 100, &S::a);
  sort_by_field(s, 100, &S::b);
}

It is easy to update the above sort_by_field function to accept custom comparator, instead of the hardcoded < comparison inside the lambda.

In the above version of sort_by_field I made pointer-to-member p a normal run-time function parameter. It is also possible to make it a compile-time template parameter. It is just a matter of finding the proper balance between run-time parameterization of the code (slower, but less code bloat) and compile-time parameterization (faster, but more code bloat).

It is also possible to implement it entirely without templates, using pure run-time parameterization, by replacing the pointer-to-member with byte offset and using qsort-style comparator callback. But this would become a significantly more involving and hackish C-style solution.

Community
  • 1
  • 1
AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • looks really cool! does this only work with std::sort()? because I am supposed to use my own sorting algorithms, for example an iterative algorithm like bubblesort, or a recursive one like quicksort – Michael Baldwin Feb 20 '16 at 00:05
  • @Michael Baldwin: It can/will work with any algorithm, of course, as long as you implement the sorting function properly, as a properly parameterized template. – AnT stands with Russia Feb 20 '16 at 00:06
  • very cool, I will see if I can get it to work. Thanks a bunch!! – Michael Baldwin Feb 20 '16 at 00:08
  • @Michael Baldwin: In other words, all you have to do is remove everything from the above `sort_by_field` and implement it manually, without calling `std::sort`. – AnT stands with Russia Feb 20 '16 at 00:14
1

The normal way to do this is by having a predicate function which inspects two objects and returns true/false whether the left-hand-side object is less-than the right-hand-side. In modern C++ you can do this with a lambda:

auto sortByYards = [](const team_stats& lhs, const team_stats& rhs) -> bool {
    return lhs.yards_per_game < rhs.yards_per_game;
};

quickSort(arr, numTeams, sortByYards);

Your quickSort function would have a fingerprint like this:

void quickSort(team_stats* arr, size_t numTeams, std::function<bool(const team_stats&, const team_stats&)> predicate) {
...
}

or you could use a using statement to make this more readable:

using QuickSortPred = std::function<bool(const team_stats&, const team_stats&)>;
void quickSort(team_stats* arr, size_t numTeams, QuickSortPred predicate) {

where you currently do the comparison left < right or right > left you would replace with

if (predicate(left, right))

(if you have any > comparisons, just switch them around to a < and then map to predicate)

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>

struct TeamStats {
    std::string name;
    float yards_per_game;
    int total_points;
};

using QuickSortPred = std::function<bool(const TeamStats&, const TeamStats&)>;
void quickSort(TeamStats* arr, size_t numTeams, QuickSortPred predicate)
{
    /// NOTE: This is NOT a complete quicksort, it's just to demonstrate
    /// the usage of predicate.

    for (size_t i = 0; i < numTeams - 1; ++i) {
        // before: if (arr[i] < arr[i+1])
        if (predicate(arr[i], arr[i+1]))
            std::swap(arr[i], arr[i+1]);
    }
}


int main() {
    TeamStats arr[] = {
        { "Red",   100, 30, },
        { "Blue",  150, 10, },
        { "Green", 200, 20, },
    };

    // approach one, store the lambda before hand
    auto sortByYards = [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.yards_per_game < rhs.yards_per_game;
    };
    quickSort(arr, 3, sortByYards);

    // approach two, write the lambda inline.
    quickSort(arr, 3, [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.total_points < rhs.total_points;
    });

    return 0;
}

Live demo: http://ideone.com/7qLtfV

And if we're going to do it properly in modern C++, we'd probably use a container other than a flat array for our sortable objects, such as a vector. If your exercise is to develop a quick sort routine, you'll want to replace 'std::sort' with your own code :)

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>

struct TeamStats {
    std::string name;
    float yards_per_game;
    int total_points;
};

using QuickSortPred = std::function<bool(const TeamStats&, const TeamStats&)>;

template<typename I>
void quickSort(I begin, I end, QuickSortPred predicate)
{
    /// NOTE: This is NOT a complete quicksort, it's just to demonstrate
    /// the usage of predicate.

    std::sort(begin, end, predicate);   
}

int main() {
    std::vector<TeamStats> arr {
        { "Red",   100, 30, },
        { "Blue",  150, 10, },
        { "Green", 200, 20, },
    };

    // approach one, store the lambda before hand
    auto sortByYards = [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.yards_per_game < rhs.yards_per_game;
    };
    quickSort(arr.begin(), arr.end(), sortByYards);
    std::cout << "By yards:\n";
    for (auto& it : arr) {
        std::cout << it.yards_per_game << " " << it.name << "\n";
    }

    // approach two, write the lambda inline.
    quickSort(arr.begin(), arr.end(), [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.total_points < rhs.total_points;
    });
    std::cout << "By points:\n";
    for (auto& it : arr) {
        std::cout << it.total_points << " " << it.name << "\n";
    }

    return 0;
}

Live demo: http://ideone.com/N35RRn

If you're not trying an exercise to write your own quicksort, you can remove all the quicksort stuff here and boil it down to

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>

struct TeamStats {
    std::string name;
    float yards_per_game;
    int total_points;
};

int main() {
    std::vector<TeamStats> arr {
        { "Red",   100, 30, },
        { "Blue",  150, 10, },
        { "Green", 200, 20, },
    };

    // approach one, store the lambda before hand
    auto sortByYards = [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.yards_per_game < rhs.yards_per_game;
    };
    std::sort(arr.begin(), arr.end(), sortByYards);
    std::cout << "By yards:\n";
    for (auto& it : arr) {
        std::cout << it.yards_per_game << " " << it.name << "\n";
    }

    // approach two, write the lambda inline.
    std::sort(arr.begin(), arr.end(), [](const TeamStats& lhs, const TeamStats& rhs) -> bool {
        return lhs.total_points < rhs.total_points;
    });
    std::cout << "By points:\n";
    for (auto& it : arr) {
        std::cout << it.total_points << " " << it.name << "\n";
    }

    return 0;
}

live demo: http://ideone.com/vDSYtj

kfsone
  • 23,617
  • 2
  • 42
  • 74
0

You can do it with a map of pointers to members and calling the variables with the pointers, maybe this could give you a hint

// typedef for the pointer-to-member
 typedef int X::*ptr_attr;

 // Declare the map of pointers to members
 map<string,ptr_attr> mattr;
 // Add pointers to individual members one by one:
 mattr["xx"] = &X::xx;
 mattr["yy"] = &X::yy;

// Now that you have an instance of x...
 X x;
// you can access its members by pointers using the syntax below:
 x.*mattr["xx"] = A["aa"];

Reference: C++ Structs: get attribute by name

Community
  • 1
  • 1
  • This would directly translate strings to member pointers, yes. However, it would only work for int types, while the struct contains more, and the asker stated that the use of strings was just an example and not what he literally wanted. – Weak to Enuma Elish Feb 20 '16 at 00:05