Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ generic class dealing with dereferencing syntax

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();
}
like image 900
Blair Davidson Avatar asked Dec 01 '22 12:12

Blair Davidson


2 Answers

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);
like image 188
Barry Avatar answered Dec 04 '22 02:12

Barry


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(); });
like image 45
Joseph Artsimovich Avatar answered Dec 04 '22 01:12

Joseph Artsimovich