4

What is the best way to deal with the fact that some types require members / methods to be accessed with the . operator whilst others with the -> operator.

Is it best to write the code for the . operator and have the caller wrap the type as show in the code sample below.

Coming from a C# background I am not use to having this particular issue.

#include <iostream>
#include <string>
#include <vector>
#include <memory>

template<class T>
class container{
  public:
    void add(T element){
        elements_.push_back(std::move(element));   
    }

    void process(){
        for(auto& a: elements_){
            a.print();   
        }
    }
  private:
    std::vector<T> elements_;
};

class printable{
public:
    void print(){
        std::cout << "Print\n";   
    }
};

template<class T>
class printable_forwarder{
public:
    printable_forwarder(T element): element_{std::move(element)}{

    }

    void print(){
        element_->print();   
    }

private:
    T element_;
};

int main()
{
    container<printable> c1;
    c1.add(printable{});
    c1.process();

   container<printable_forwarder<std::shared_ptr<printable>>> c2;
   std::shared_ptr<printable> sp{std::make_shared<printable>()};
   c2.add(printable_forwarder<decltype(sp)>{sp});
   c2.process();
}

Does this appear better?

#include <iostream>
#include <string>
#include <memory>
#include <type_traits>
#include <vector>
template<typename T>
class dereference
{
public:
    inline static T& get(T& value){
        return value;
    }
};

template<typename T>
class dereference<T*>
{
public: 
    inline static typename std::add_lvalue_reference<typename std::remove_pointer<T>::type>::type get(T* value){
        return *value;
    }
};

template<typename T>
class dereference<std::shared_ptr<T>>
{
public: 
    inline static T& get(std::shared_ptr<T> value){
        return *value.get();
    }
};

template<class T>
class container{
public:
    void add(T const& v){
        items_.push_back(v);   
    }

    void print_all(){
        for(auto& a: items_){
            dereference<T>::get(a).print();   
        }
    }
private:
    std::vector<T> items_;
};

struct printable{
    void print(){
        std::cout << "Printing\n";   
    }
};

int main()
{
    container<printable> c1;
    c1.add(printable{});
    c1.print_all();

    container<std::shared_ptr<printable>> c2;
    c2.add( std::shared_ptr<printable>(new printable{}));
    c2.print_all();
}
Blair Davidson
  • 901
  • 12
  • 35

6 Answers6

13

What is the best way to deal with the fact that some types require members / methods to be accessed with the . operator whilst others with the -> operator.

Just don't.

Your job is to write template<class T> class container. That container holds Ts. If your users want to do something on the T, you should expose the ability to do something - but it is their responsbility to perform that action properly. Otherwise, you're just adding a ton of code bloat. Great, you gave me a way to print all the elements, but what if I know what to call foo() on them, or find the first element for which bar() returns something bigger than 42? Clearly, you're not going to write for_each_foo() and find_if_bar_is_42().

This is why the standard library separates containers from algorithms. The way to make your container as usable as possible is to have it expose two iterators via begin() and end(), and then I can just do whatever I need to do as the user:

container<T> values;
values.add(...);

// I know to use '.'
for (T& t : values) {
   t.print();
} 

container<T*> pointers;
pointers.add(...);

// I know to use '->'
for (T* t : pointers) {
    t->print();
}

auto iter = std::find_if(pointers.begin(), pointers.end(), [](T* t){
    return t->bar() == 42;
});

Barring that, you can add a bunch of member functions that themselves take callables, so you pass on the work to the user:

template <class F>
void for_each(F&& f) {
    for (auto& elem : elements_) {
        f(elem);              // option a
        std::invoke(f, elem); // option b, as of C++17
    }
}

so the above examples would be:

values.for_each([](T& t){ t.print(); });
pointers.for_each([](T* t){ t->print(); });
values.for_each(std::mem_fn(&T::print));
pointers.for_each(std::mem_fn(&T::print));

Note that it's always up to the user to know what to do. Also, if you use std::invoke() in the implementation of for_each, then you could just write:

pointers.for_each(&T::print);
values.for_each(&T::print);

and, for that matter:

container<std::unique_ptr<T>> unique_ptrs;
unique_ptrs.for_each(&T::print);
Barry
  • 286,269
  • 29
  • 621
  • 977
2

As an alternative to parametrising container with a printer type as suggested in another answer, I'd suggest parametrising the container::process() method instead:

template<typename F>
void process(F&& func)
{
    for (auto& e : elements)
    {
        func(e);
    }
}

Then the client code would look like this:

container<printable> value_container;
value_container.add(...);
value_container.process([](printable& obj) { obj.print(); });

container<printable*> ptr_container;
ptr_container.add(...);
ptr_container.process([](printable* obj) { obj->print(); });
Joseph Artsimovich
  • 1,499
  • 10
  • 13
2

Blair,

I think a more idiomatic modern approach would be to use a traits type. This pattern allows the library author to create a protocol that will be implemented by the library in common cases, but is extensible so that clients can support any necessary case.

In the code below I've put the container class in a namespace called library and defined the printable class in a namespace called client. I also, to demonstrate the client extensibility of this pattern, created a new client type called other_printable which supports the functionality we want (it prints), but has a different API for it (there is a free standing print, rather than member function print).

The traits class, print_traits, is just a type template with full or partial specializations, some provided by the library and potentially some provided by the client. In this case, the primary template has an implementation (it calls the print member function). Some times in this pattern, there is no primary implementation and every case is a specialization.

The use cases the library wants to support are:

  1. types with print member functions
  2. pointers to supported types
  3. std::unique_ptr to supported types
  4. std::shared_ptr to supported types

So in addition to the primary template that supports case 1, the library author provides specializations for the other three cases (the specializations in the library namespace.)

Since the client wants to use a type that doesn't follow the library's supported API (a print member), the client simply creates a print_traits specialization to handle the unsupported API (the free standing print function).

Note that by adding this specialization we make other_printable a supported type so that it we can created containers that hold pointers (including smart pointers) to it.

Note also that a specialization template definition, but be in the same namespace as the primary template that it specializes. This means that the client code must open the library namespace to specialize print_traits.

Here is the code:

#include <iostream>
#include <vector>
#include <memory>

// as if included from library.hpp
namespace library
{
template <class T>
struct print_traits
{
    static void print(T const& t)
    {
        t.print();
    }
};

template <class T>
struct print_traits<T*>
{
    static void print(T* p)
    {
        print_traits<T>::print(*p);
    }
};

template <class T>
struct print_traits<std::unique_ptr<T>>
{
    static void print(std::unique_ptr<T>const& p)
    {
        print_traits<T>::print(*p);
    }
};

template <class T>
struct print_traits<std::shared_ptr<T>>
{
    static void print(std::shared_ptr<T>const& p)
    {
        print_traits<T>::print(*p);
    }
};


template<class T>
struct container
{
    void insert(T element)
    {
        elements_.push_back(std::move(element));   
    }

    void process()
    {
        for (auto const& a: elements_)
        {
            print_traits<T>::print(a);
        }
    }
  private:
    std::vector<T> elements_;
};
}

// as if included from client.hpp (which would include library.hpp)
namespace client
{
    struct printable
    {
        void print() const
        {
            std::cout << "Print\n";
        }
    };

    struct other_printable {};

    void print(other_printable const&op)
    {
        std::cout << "Print\n";
    }

}

// template specializations must be in the same namespace as the primary
namespace library
{
    template <>
    struct print_traits<client::other_printable>
    {
        static void print(client::other_printable const& op)
        {
            client::print(op);
        }
    };
}

// main.cpp includes client.hpp
int main()
{
    using client::printable;
    using client::other_printable;
    using library::container;

    printable p0;


    container<printable> c0;
    c0.insert(p0);
    c0.process();

    container<printable*> c1;
    c1.insert(&p0);
    c1.process();

    container<std::unique_ptr<printable>> c2;
    c2.insert(std::make_unique<printable>());
    c2.process();

    container<std::shared_ptr<printable>> c3;
    c3.insert(std::make_shared<printable>());
    c3.process();

    other_printable op;

    container<other_printable> c4;
    c4.insert(op);
    c4.process();

    container<std::unique_ptr<other_printable>> c5;
    c5.insert(std::make_unique<other_printable>());
    c5.process();

}

I feel compelled to point out that this kind of thing doesn't come up that often in C++, because we don't normally want to treat objects and the things that point to them in the same way. That said, I hope this demonstrates an approach that could be used to accomplish that in a particular case.

Jon Kalb
  • 266
  • 1
  • 5
  • What do you mean "we dont normally want to treat object and things that point to them in the same way" – Blair Davidson Jan 17 '17 at 00:47
  • Also if you were to design an alternative solution what would you recommend. – Blair Davidson Jan 17 '17 at 01:28
  • Not really a trait is it in the sense of std::is_pointer::value ?? – Blair Davidson Jan 17 '17 at 12:21
  • @BlairDavidson You seem to have a very specific use case (container of things with a `print` function) but for some reason you want your container to be compatible with both `printable` and `shared_ptr`. I would have expected your specific use case to require one or the other. Why do you need to support both? – Joseph Thomson Jan 18 '17 at 06:59
  • Why would I not want to support both – Blair Davidson Jan 18 '17 at 07:02
  • @BlairDavidson "Why would I not want to do X?" is not a reason to do X. Your code doesn't support types where the `print` function is instead named `output`. Why wouldn't you want to support this use case as well? What is your use case? What problem are you trying to solve? – Joseph Thomson Jan 19 '17 at 07:19
  • @BlairDavidson: "Also if you were to design an alternative solution what would you recommend?" I'm not really certain what problem you are trying to solve. This solution works for the problem as proposed "I want to make a container that 'processes' pointers to objects as if they are the objects pointed to." – Jon Kalb Jan 19 '17 at 17:55
  • 2
    @BlairDavidson "Not really a trait is it in the sense of std::is_pointer::value ??" The defining characteristic of the traits class pattern is not the type of the trait, it is that third element is introduced (the trait class) that supports customization so that to types (neither of which is modified) can work with each other. This can be done because one of the two types (the library) is designed with the traits class in mind. The fact that we usually use traits classes for type definitions doesn't preclude us from using it for functionality. – Jon Kalb Jan 19 '17 at 17:59
1

This is just a warning in trying to automate the process to check if the type stored by the container is a pointer type that happens to be an actual smart pointer. This warning is coming from boost documentation:

Important is_pointer detects "real" pointer types only, and not smart pointers. Users should not specialise is_pointer for smart pointer types, as doing so may cause Boost (and other third party) code to fail to function correctly. Users wanting a trait to detect smart pointers should create their own. However, note that there is no way in general to auto-magically detect smart pointer types, so such a trait would have to be partially specialised for each supported smart pointer type.

That can be found here. I think that this is relevant to the problem at hand in general as it doesn't exactly answer the question where it is just a tip or something good to know ahead of time to help in the decision process of designing one's source code.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
0

I'm not sure if it's the best solution, but you can make those four overloads of one function:

template<typename T>
T& dereference(T& obj) {
    return obj;
}

template<typename T>
T& dereference(std::shared_ptr<T> obj) {
    return *obj;
}

template<typename T>
T& dereference(std::unique_ptr<T> obj) {
    return *obj;
}

template<typename T>
T& dereference(T* obj) {
    return *obj;
}

Now you can pass to it any object, smart pointer (except of deprecated std::auto_ptr) or raw pointer:

int main() {
    int i = 3;
    auto ptr = std::make_shared<int>(5);

    std::cout << dereference(i) << ", " << dereference(ptr) << std::endl;

    return 0;
}

This will print 3, 5.

But IMHO using callback would be cleaner.

template<class T>
class container {
private:
    std::vector<T> elements_;
    std::function<void(const T&)> callback_;

public:
    template<typename callback_t>
    container(callback_t callback) {
        callback_ = callback;
    }

    void add(T element){
        elements_.push_back(std::move(element));
    }

    void process() {
        for(auto& a: elements_){
            callback_(a);
        }
    }
};

Now you'll be able to pass callback in constructor:

container<int> c([](const int& val) {
    std::cout << val << std::endl;
});

c.add(3);
c.add(56);
c.add(4);

c.process();

Remeber that you need to include functional header to use std::function.

Kamil Koczurek
  • 696
  • 5
  • 15
  • This is not a bad answer, Alex Stepanov himself once said that types that are not pointer-like should reference to themselves. One way to achieve that uniformity is something like this. – alfC Jan 02 '22 at 08:31
0

In this situation, the neatest solution I can think of to avoid duplicating code is to allow the user to provide an optional function object to do the printing. For example:

template <typename T>
struct default_print {
  void operator()(T& t) {
    t.print();
  }
};

template <typename T, typename Printer = default_print<T>>
class container {
public:
  container() = default;

  container(Printer p) : printer(p) {
  }

  void add(T const& element) {
    elements.push_back(element);   
  }

  void process() {
    for (auto& e : elements) {
      printer(e);   
    }
  }

private:
  Printer printer;
  std::vector<T> elements;
};

This is much like how std::unique_ptr allows a custom deleter to be specified. You could use the empty base class optimization to get zero size overhead for a stateless printer if you wanted.

You would use container like so:

struct printable {
  void print() {}
};

template <typename T>
struct indirect_print {
  void operator()(T& t) {
    t->print();
  }
};

int main() {
  container<printable> c1;
  c1.process();

  container<printable*, indirect_print<printable*>> c2;
  c2.process();
}

If you don't like the typing, you could use some SFINAE to implement a utility function that automatically uses a different printer if T has the -> operator:

template <typename>
using void_t = void;

template <typename T, typename = void>
struct has_arrow_operator : std::false_type {
};

template <typename T>
struct has_arrow_operator<T, void_t<
    decltype(std::declval<T>().operator->())>> : std::true_type {
};

template <typename T>
struct has_arrow : std::integral_constant<bool,
    std::is_pointer<T>::value || has_arrow_operator<T>::value> {
};

template <typename T, typename = std::enable_if_t<!has_arrow<T>::value>>
container<T> make_container() {
  return container<T>();
}

template <typename T, typename = std::enable_if_t<has_arrow<T>::value>>
container<T, indirect_print<T>> make_container() {
  return container<T, indirect_print<T>>();
}

You would use make_container like so:

int main() {
  auto c1 = make_container<printable>();
  c1.process();

  auto c2 = make_container<printable*>();
  c2.process();
}

You could always use SFINAE to perform the printer switching directly in the container class, but I feel that keeping the class as generic as possible, and encapsulating your use case in a utility function is a cleaner design.

Joseph Thomson
  • 9,888
  • 1
  • 34
  • 38
  • Pardon my ignorance, but Joseph, I would appreciate it if you could help me understand this syntax in your answer (my c++ knowledge is passable, but not my strongest) `decltype(std::declval().operator->())>> : std::true_type`, I understand you are using SFINAE to determine if the receiver is a pointer, I can see what you are doing, but dont understand the decltype syntax posted. Thanks in advance. – Jake H Jan 19 '17 at 18:49
  • 1
    @JakeHeidt This is expression SFINAE. The `decltype` is used to turn the expression into a type, so that it can form part of a template argument. If the expression is not valid (in this case, if `T` does not implement `operator->()`) then the substitution will fail and the specialization will not be used. `void_t` is required to allow substitution to happen, just like `enable_if_t`. – Joseph Thomson Jan 20 '17 at 02:54