1

I am fairly new to std::tuple and std::tie. I need a way to efficiently order structs according to a left to right ordering of comparisons. For this reason, I chose to use the std::make_tuple and std::tie types for custom ordering a StructA in the live example provided. The tuple approach provides built in equivalence comparisons starting from left to right which is ideal for LessThanComparable element ordering for a std::sort with an accompanying lambda comparator (for which I show 3 examples).

The problem is that as far as I am aware, std::make_tuple makes inefficient copies of the tuple elements, and I was wondering if there was some way to combine std::make_tuple with std::tie as I tried to do with my 3rd comparator - which failed (otherwise its output would look like the first output ordering).

In my specific example I cannot use std::tie directly as I need to use a temporary as the 1st element in my tuple.

The output is as follows

Order[mPath.filename(), elem1, intVal]
======================================
"/zoo/dir1/filename.txt" - nameA1 - 1
"/tmp/dir1/filename.txt" - nameA1 - 3
"/fad/dir1/filename.txt" - nameA1 - 4

Order[mPath, elem1, intVal]
======================================
"/fad/dir1/filename.txt" - nameA1 - 4
"/tmp/dir1/filename.txt" - nameA1 - 3
"/zoo/dir1/filename.txt" - nameA1 - 1

Order[mPath.filename(), elem1, intVal]
======================================
"/fad/dir1/filename.txt" - nameA1 - 4
"/tmp/dir1/filename.txt" - nameA1 - 3
"/zoo/dir1/filename.txt" - nameA1 - 1

I was expecting that the 3rd set of outputs would be identical to the first or alternatively if someone could tell me how to correctly mix inefficient std::tuples with efficient std::ties

#include <iostream>
#include <string>
#include <vector>
#include <tuple>
#include <boost/filesystem.hpp>

struct StructA {
    boost::filesystem::path mPath;
    std::string elem1;
    int intVal;
};

template<typename CharT, typename Traits>
std::basic_ostream<CharT, Traits>& 
operator<<(std::basic_ostream<CharT, Traits>& os, StructA const& sa) {
    return os << sa.mPath << " - " << sa.elem1 << " - " << sa.intVal << std::endl;
}


int main()
{
    std::vector<StructA> aStructs = {
        {"/zoo/dir1/filename.txt", "nameA1", 1}, 
        {"/fad/dir1/filename.txt", "nameA1", 4}, 
        {"/tmp/dir1/filename.txt", "nameA1", 3}
    };    

    std::cout << "Order[mPath.filename(), elem1, intVal]" << std::endl;
    std::cout << "======================================" << std::endl;
    std::sort(aStructs.begin(), aStructs.end(),
        [](const StructA& lhs, const StructA& rhs){
            return std::make_tuple(lhs.mPath.filename(), lhs.elem1, lhs.intVal) < 
                std::make_tuple(rhs.mPath.filename(), rhs.elem1, rhs.intVal);
        });

    // print reordered structs
    std::copy(aStructs.begin(), aStructs.end(),
        std::ostream_iterator<StructA>(std::cout, ""));        

    std::cout << std::endl;

    std::cout << "Order[mPath, elem1, intVal]" << std::endl;
    std::cout << "======================================" << std::endl;
    std::sort(aStructs.begin(), aStructs.end(),
        [](const StructA& lhs, const StructA& rhs){
            return std::tie(lhs.mPath, lhs.elem1, lhs.intVal) < 
                std::tie(rhs.mPath, rhs.elem1, rhs.intVal);
        });

    // print reordered structs
    std::copy(aStructs.begin(), aStructs.end(),
        std::ostream_iterator<StructA>(std::cout, ""));

    std::cout << std::endl;

    std::cout << "Order[mPath.filename(), elem1, intVal]" << std::endl;
    std::cout << "======================================" << std::endl;
    std::sort(aStructs.begin(), aStructs.end(),
        [](const StructA& lhs, const StructA& rhs){
            // attempt at efficiency - but not quite right
            return lhs.mPath.filename() < rhs.mPath.filename() && 
                std::tie(lhs.elem1, lhs.intVal) < std::tie(rhs.elem1, rhs.intVal);
        });

    // print reordered structs
    std::copy(aStructs.begin(), aStructs.end(),
        std::ostream_iterator<StructA>(std::cout, ""));
}
Praetorian
  • 106,671
  • 19
  • 240
  • 328
johnco3
  • 2,401
  • 4
  • 35
  • 67
  • 1
    What about [`forward_as_tuple`](http://en.cppreference.com/w/cpp/utility/tuple/forward_as_tuple)? – dyp May 13 '14 at 23:34
  • 1
    You can still use `std::tie` with some `as_lvalue`, see e.g. http://stackoverflow.com/q/23566857/420683 – dyp May 13 '14 at 23:37
  • Assign the return values from `filename()` calls to local variables, and then use `std::tie`. Not a very scalable solution, but not bad when you're only dealing with 2 such variables. – Praetorian May 13 '14 at 23:40
  • Actually I'm not quite sure how to use forward_as_tuple - but I did find a way to make my 3rd lambda to match the output of the first comparison - effectively making just 1 of the elements in the tuple inefficient. What I failed to do in my 3rd lambda was to compare correctly using (3) in the following http://en.cppreference.com/w/cpp/utility/tuple/operator_cmp – johnco3 May 13 '14 at 23:40
  • @Praetorian, std::tie takes lvalue references - that will not work, that is why my 3rd lambda used lhs.mPath.filename() < rhs.mPath.filename() && std::tie(lhs.elem1, lhs.intVal) < std::tie(rhs.elem1, rhs.intVal); See my comment above though - this was flawed – johnco3 May 13 '14 at 23:43
  • 2
    @johnco3 What I meant was `auto f1 = lhs.mPath.filename(); auto f2 = rhs.mPath.filename(); return std::tie(f1, lhs.elem1, lhs.intVal) < std::tie(f2, rhs.elem1, rhs.intVal);` – Praetorian May 13 '14 at 23:46
  • 1
    @Praetorian - Fantastic - that was the answer I was searching for!! Nice job!! – johnco3 May 13 '14 at 23:53

2 Answers2

3
std::tuple<std::string, std::string const&, int>
sort_helper(StructA const& s){
  return{ s.mPath.filename(), s.elem1, s.intVal };
}

then:

std::sort(aStructs.begin(), aStructs.end(),
  [](const StructA& lhs, const StructA& rhs){
    return sort_helper(lhs)<sort_helper(rhs);
  }
);

which seems much cleaner no?

At the cost of one std::string move, basically.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

I just discovered that the problem with the 3rd lambda was that I was not correctly comparing the elements correctly for equivalence and lexicographical comparison. The correct approach is outlined in bullet 3) for tuple comparison where it indicates that I should use the following approach.

3) (bool)(std::get<0>(lhs) < std::get<0>(rhs)) || (!(bool)(std::get<0>(rhs) < std::get<0>(lhs)) && lhstail < rhstail), where lhstail is lhs without its first element, and rhstail is rhs without its first element. For two empty tuples, returns false.

The fixed up lambda comparator for sorting firsty sorts according to the filename() temporary and then uses the efficient std::tie for the other elements in the tuple

std::cout << "Order[mPath.filename(), elem1, intVal]" << std::endl;
std::cout << "======================================" << std::endl;
std::sort(aStructs.begin(), aStructs.end(),
    [](const StructA& lhs, const StructA& rhs){
        // attempt at efficiency - but not quite right
        // AHA, I think I figured it out - see tuple operator_cmp
        // return value documentation which states 
        // (bool)(std::get<0>(lhs) < std::get<0>(rhs)) || 
        // (!(bool)(std::get<0>(rhs) < std::get<0>(lhs)) && 
        // lhstail < rhstail), where lhstail is lhs without 
        // its first element, and rhstail is rhs without its 
        // first element. For two empty tuples, returns false.
        // --------------------------------------------------------
        // edit thanks to @Praetorian for suggesting the following:
        // --------------------------------------------------------
        auto f1 = lhs.mPath.filename(); auto f2 = rhs.mPath.filename();            
        return std::tie(f1, lhs.elem1, lhs.intVal) < std::tie(f2, rhs.elem1, rhs.intVal);
    });

Doing so made the first set of results identical to the 3rd - not incredibly efficient for the filename() temporary but at least I don't take the std::make_tuple hit for all elements in my struct. The updated live example is here

johnco3
  • 2,401
  • 4
  • 35
  • 67
  • Did you actually profile an optimized build? I would think a good compiler should be able to optimize away the copies in the make_tuple() version and according to the comments on this answer that appears to be the case: http://stackoverflow.com/a/6219150/139091 – mattnewport May 14 '14 at 00:12
  • No I never got a chance to test the performance difference, One the links in the original question indicated that an extra copy would be needed to put make_tuple, that being said the implementation I see in Visual C++ 2013 is as follows so it would appear to perfect forward temporaries // FUNCTION make_tuple template inline tuple::type...> make_tuple(_Types&&... _Args) { // make tuple from elements typedef tuple::type...> _Ttype; return (_Ttype(_STD forward<_Types>(_Args)...)); } – johnco3 May 14 '14 at 02:59