13

I have the following code to test smart pointer as key for std::map, I run the code on Mac and Linux, but I observed different output, is it a bug or have I done anything wrong?

#include <iostream>
#include <memory>
#include <string>
#include <map>

using namespace std;

class Dog {
 public:
   typedef shared_ptr<Dog> sptr;

   Dog(const string &name) : name_(name) { }

   friend bool operator<(const Dog &lhs, const Dog &rhs) {
     cout << "Dog::operator< called" << endl;
     return lhs.name_ < rhs.name_;
   } 

   friend bool operator<(const sptr &lhs, const sptr &rhs) {
     cout << "Dog::operator< sptr called" << endl;
     return lhs->name_ < rhs->name_;
   } 

 private:
   string name_;
};

void test_raw_object_as_map_key() {
  cout << "raw object as map key ============== " << endl;
  map<Dog, int> m;
  m[Dog("A")] = 1;
  m[Dog("B")] = 2;
  m[Dog("C")] = 3;
  m[Dog("A")] = 4;

  cout << "map size: " << m.size() << endl;
}

void test_smart_pointer_as_map_key() {
  cout << "smart pointer as map key ============== " << endl;

  map<Dog::sptr, int> m;
  m[make_shared<Dog>("A")] = 1;
  m[make_shared<Dog>("B")] = 2;
  m[make_shared<Dog>("C")] = 3;
  m[make_shared<Dog>("A")] = 4;

  cout << "map size: " << m.size() << endl;
}

int main(int argc, const char *argv[]) {
  test_raw_object_as_map_key();
  test_smart_pointer_as_map_key();
  return 0;
}

On Mac:

neevek@MAC$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix

neevek@MAC$ ./a.out
raw object as map key ============== 
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
map size: 3
smart pointer as map key ============== 
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
Dog::operator< sptr called
map size: 3

On Linux:

neevek@LINUX$ g++ --version
g++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

neevek@LINUX$ ./a.out
raw object as map key ============== 
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
Dog::operator< called
map size: 3
smart pointer as map key ============== 
map size: 4
Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
neevek
  • 11,760
  • 8
  • 55
  • 73
  • btw, the second `operator <` doesn't need to be a friend, it can just be `return *lhs < *rhs;` – o11c Oct 08 '14 at 08:55
  • @o11c, yes, but the change makes no difference. – neevek Oct 08 '14 at 09:02
  • Possibly related: http://stackoverflow.com/questions/11115265/clang-stdshared-ptr-and-stdless-operator – Chris Culter Oct 08 '14 at 09:07
  • 1
    The key point is that there is an `operator<` for `std::shared_ptr` for any `T` in `std`, and you've now provided an overload for `std::shared_ptr`, which can only be found by ADL. I don't know which compiler is picking the correct one, or if there's UB involved. – Angew is no longer proud of SO Oct 08 '14 at 09:27

2 Answers2

8

GCC is right (on Mac as you see g++ is in fact clang): std::map uses std::less<T> to compare keys. That in turn calls operator < on the arguments, but the lookup is performed first in namespace std, so it finds the default implementation for shared_ptr, comparing internal pointers. To make this work you have to specialize std::less for shared_ptr<Dog>:

namespace std {
    template<>
    struct less<shared_ptr<Dog>> {
        bool operator() (const shared_ptr<Dog>& lhs, const shared_ptr<Dog>& rhs) {
            return *lhs < *rhs;
        }
    };
}
Anton Savin
  • 40,838
  • 8
  • 54
  • 90
4

The default Compare object of std::map is std::less<Key>, which is std::shared_ptr<Dog> in your case. So it looks for std::less< std::shared_ptr<Dog> > implementation, and it just compares the pointer address.

To specify the Compare object, you can just define it by yourself and use it in map

class MyCompare {
public:
  bool operator() (const sptr& l, const sptr& r) {
    return *l < *r; // Invokes your Dog's operator <
  }
};

Then

  map<sptr, int, MyCompare> m;
  m[make_shared<Dog>("A")] = 1;
  m[make_shared<Dog>("B")] = 2;
  m[make_shared<Dog>("C")] = 3;
  m[make_shared<Dog>("A")] = 4;

  cout << "map size: " << m.size() << endl;

outputs map size: 3

Mine
  • 4,123
  • 1
  • 25
  • 46