1

Can this code be considered good design?

It compiles and works fine, both with GCC and Visual Studio. The slot object ends up really small, neatly packed and easy to reason about.

However, how much the casting will slow down my program in the end?

If I ended up using boost::any or boost::variant I would still put them in a std::shared_ptr because I do need it to be a smart pointer.

I'm using C++14.

//EDITED BEGIN

Context: I'm builing an interpreter, something in the vein of lisp / ruby. I want slot objects to be a sort of god object. I want to be able to construct and return new slot objects inside the slot object itself, both shallow and deep copies. The data in the slot object is a pointer because I intend that data to be shared between objects. The slot_t enumerator exists mainly to be used in switch statements and it needs to be accessible outside the slot class, that's why it was made global. The slot_t in the constructor is needed because sometimes a type can have the same internal representation, and thus a mean to disambiguation is needed. The example I gave was rushed and it does have some problems.

This is how I envision my slot object:

4 members -> gate (variable / constant, ...), type, data and docstring.
Operator overloads -> bool, ==, !=, <, > <=, >=.
Some methods: copy, deep_copy, get_type(by returning a new slot), etc...
I think you did get the point. :D

This is my rushed example:

//EDITED END

#include <iostream>
#include <memory>
#include <sstream>
#include <string>

using std::cout;
using std::endl;

enum class slot_t {
    number_t,
    string_t
};

class slot {
public:
    slot_t type;
    std::shared_ptr<void> data;
    slot(slot_t const p_type, double long const & p_data)
        : type {p_type}
        , data {std::make_shared<double long>(p_data)}
    {}
    slot(slot_t const p_type, std::string const & p_data)
        : type {p_type}
        , data {std::make_shared<std::string>(p_data)}
    {}
    std::string get_type() const {
        std::ostringstream output;
        switch (type) {
            case slot_t::string_t: output << "String: " << as<std::string>(); break;
            case slot_t::number_t: output << "Number: " << as<double long>(); break;
        }
        return output.str();
    }
    template <typename t>
    t as() const {
        return *std::static_pointer_cast<t>(data);
    }
};

int main() {
    slot hello {slot_t::number_t, 123};
    slot world {slot_t::string_t, "Hello, world!"};

    cout << hello.as<double long>() << endl;
    cout << world.as<std::string>() << endl;

    cout << hello.get_type() << endl;
    cout << world.get_type() << endl;
    return 0;
}
João Pires
  • 927
  • 1
  • 5
  • 16
  • 1
    I think the constructors could take just one parameter, and the `slot_t` is implied once the constructor is decided, i.e in the constructor which takes `double` as single argument, the `slot_t` is implied to be `number_t`. I mean, `slot hello {slot_t::number_t, "hi"}` does **not** make sense, so why give the freedom to pass `slot_t` whose value depends on *the type* of the *second* argument. – Nawaz Nov 29 '16 at 18:05
  • I'm building an interpreter, and in this case, different data types can have the same internal representation. Ex, in my language I will implement these types wich I named them: tag, token, string and char - they all use a string as its internal representation - I need some way to distinguish between them. – João Pires Nov 29 '16 at 18:35

2 Answers2

2

This really looks like a job for variant. In effect, what you have written is a poorly written shared variant.

There are a number of problems. The first of which is that you pass the type separately from the type in the ctor. The second is you have poor visiting code.

So some tweaks:

// sink variables: take by value, move into storage:
slot(double long p_data)
    : type {slot_t::number_t}
    , data {std::make_shared<double long>(p_data)}
{}
// sink variables: take by value, move into storage:
slot(std::string p_data)
    : type {slot_t::string_t}
    , data {std::make_shared<std::string>(std::move(p_data))}
{}
// get type from the type, not from a separate variable.

// boost and std style visit function:
template<class F>
auto visit( F&& f )
-> typename std::result_of< F(int&) >::type
{
  switch(type) {
    case slot_t::string_t: return std::forward<F>(f)( as<std::string>() );
    case slot_t::number_t: return std::forward<F>(f)( as<double long>() );
  }
}
// const visit:
template<class F>
auto visit( F&& f ) const
-> typename std::result_of< F(int const&) >::type
{
  switch(type) {
    case slot_t::string_t: return std::forward<F>(f)( as<std::string>() );
    case slot_t::number_t: return std::forward<F>(f)( as<double long>() );
  }
}
// const and non-const as that return references:
template <typename t>
t const& as() const {
    return *static_cast<t const*>(data.get());
}
template <typename t>
t& as() {
    return *static_cast<t*>(data.get());
}

Near enum type_t:

inline std::string get_typename(type_t type) {
  switch (type) {
    case type_t::string_t: return "String";
    case type_t::number_t: return "Number";
  }
}

as name is a property of type_t, not of your slot type.

A C++14 implementation of get_type, where we create a visitor as a lambda:

std::string get_type() const {
  std::ostringstream output;
  output << get_typename(type) << ": ";
  return visit( [&](auto&& value) {
    output <<  value; // notice not casting here
  } );
  return output.str();
}

This visit pattern mimics how boost::variant works, and is what you should pattern your code after. In C++11 you do have to write your visitor class separately, not as a lambda.

Of course:

struct slot {
  std::shared_ptr< boost::variant<std::string, long double> > pImpl;
};

is a much better chassy. Now we just have to map your enums and operations over to operating on the variant inside the shared ptr.

However, another rule of thumb is that shared state is bad. It is almost as bad as global state; it makes programs hard to reason about. I'd do away with the shared state, and replace it with a naked variant.

Both ints and long doubles are cheap to copy. A variant containing them is also cheap to copy.


visit is powerful. But sometimes you want completely different code based on the type of the object. This works:

template<class...Ts>
struct overload_t {
private:
  struct never_used {};
public:
  void operator()(never_used)=delete;
};
template<class T0, class...Ts>
struct overload_t: T0, overload_t<Ts...> {
  overload_t(T0 t0, Ts...ts):
    T0(std::move(t0)),
    overload_t<Ts...>(std::move(ts)...)
  {}
  overload_t(overload_t&&)=default;
  overload_t(overload_t const&)=default;
  overload_t& operator=(overload_t&&)=default;
  overload_t& operator=(overload_t const&)=default;

  using T0::operator();
  using overload_t<Ts...>::operator();
};
template<class...Fs>
overload_t<Fs...>
overload(Fs...fs) {
  return {std::move(fs)...};
}

now we build overloads:

auto detect_type = overload(
  [](std::string const&){ std::cout << "I am a string\n"; },
  [](long double){ std::cout << "I am a number\n"; }
};
slot.visit(detect_type);

and overload resolution kicks in, resulting in the right type-safe function being called.

If you want to ignore some types, just do this:

auto detect_type = overload(
  [](std::string const&){ std::cout << "I am a string\n"; },
  [](auto const&){ std::cout << "I was ignored\n"; }
};

and again, let overload resolution solve your problems.

Isolate your type-unsafe operations to a narrow part of your code base. Do not force users to get types exactly right or generate undefined behavior every time they interact with your variant type.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Wow, your approach is a bit more complex than mine, I'm still trying to understand it. I need the `shared_ptr` because I'm building an interpreter and I want my language to work on references rather than on values. Constants would be achieved by deep-copying an object and locking it with an enumerator flag making it a 100% unique. In my old design I had lots of `shared_ptr`, one for each type, and well, I didn't like it. And the get_type was just an example to see if I could use the as method in a switch statement. – João Pires Nov 29 '16 at 18:10
  • I noticed now that I passed a double by reference, oh my, I don't usually do those kind of mistakes. xD – João Pires Nov 29 '16 at 18:16
  • @dago that does not matter; the string in the other hand should be passed by value then moved into storage. – Yakk - Adam Nevraumont Nov 29 '16 at 18:24
  • Can you explain me the purpose and why do I need a visit method, the visitor thing? I think I get the rest of the changes? And what's the difference between the `static_pointer_cast` I had and your approach? – João Pires Nov 29 '16 at 18:39
  • What about other types like maps, lists, vectors? Is it wrong to pass them by `const &`, can I pass them by `&&`? – João Pires Nov 29 '16 at 18:49
  • Wouldn't pass by value slow a lot more my program? I intend to pass on some really heavy nested containers. Also, if your `as` method now returns references, would it be safe to construct new slot objects inside the slot class having some constructors accepting passing by `const &` or `&&`? – João Pires Nov 29 '16 at 19:02
  • @DagobertoPires https://stackoverflow.com/questions/21605579/how-true-is-want-speed-pass-by-value -- in essence, movable sink types passed by value are relatively optimal. `static_pointer_cast` creates and increments a reference count atomically, mine does not. If something is cheap-to-move (like all `std` types), taking by-value and moving into storage is essentially as cheap, and sometimes much cheaper than, taking by `const&` and copying into storage – Yakk - Adam Nevraumont Nov 29 '16 at 19:26
  • What about the visit method? – João Pires Nov 29 '16 at 19:32
  • @DagobertoPires It is from `std::variant` and `boost::variant`. It lets you type-safely examine the contents of your variant (which your code is) without having to write a custom `switch` statement each time. The `f` passed in is given the contents of the variant as its argument, and it has the correct type. I used it to implement `get_type` -- notice `get_type` has one switch to print the name (which is type safe), and one call to `visit` that prints the conents to the stream *regardless of what the type of the contents is*, in a type-safe manner. – Yakk - Adam Nevraumont Nov 29 '16 at 19:45
  • We move the un-type-safe switch to *one* location, within `visit`, and we can write a whole pile of other type-safe code that *uses* visit. – Yakk - Adam Nevraumont Nov 29 '16 at 19:47
  • @DagobertoPires See `overload` added to above post. – Yakk - Adam Nevraumont Nov 29 '16 at 20:20
  • So, is visit like a more neatly packed `as`? Something like, I can give it any slot of any type and it will give me the contents of my type without having to make an explicit cast in the function? Something like my_slot.get() and this would either give me a double or a string? – João Pires Nov 29 '16 at 20:23
  • The thing is, I think I don't need that much type safety, I'm building an interpreter, people will interact with C++ throught a different language. Some problems will be caught on the tokenizer, others on the parser. All slot objects will be perfect from the beginning. I just want my class to be simple, small, easy, readable and the most self-contained possible. I don't want inheritance or complicated methods, everything must be simple and fast, I want to strip as much lines as I can and as much fancy stuff as I can! – João Pires Nov 29 '16 at 20:34
  • Some of your recomendations seems a bit too much for the context in which I would use it. Correct me if I'm wrong. – João Pires Nov 29 '16 at 20:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/129381/discussion-between-dagoberto-pires-and-yakk). – João Pires Nov 29 '16 at 21:04
0

Really have to question your design choices. So your first question, "Can this code be considered good design?" I have to say probably not. Public variables are bad, m-kay? Your class is a slut.

The cost of creating a shared_ptr in your as function is going to be about the only cost you have. You could get rid of it by using get and static_cast rather than static_pointer_cast. The static_cast is performed at compile time and should have no runtime impact.

In the end though you need to profile to answer your question. Anything else is an assumption.

Edward Strange
  • 40,307
  • 7
  • 73
  • 125
  • In the final design, data would be private. However, type would still be public because I still need access to it. The standard defines `static_pointer_cast` as something like `static_cast::element_type*>(r.get())`, isn't that similar to your approach, or am I forgetting something? – João Pires Nov 29 '16 at 17:56
  • No, the standard defines it as returning a `shared_ptr` that owns that value. That creates+destroys another `shared_ptr` object, which increments+decrements the reference count, which might use relatively expensive atomic operations. Simply using `static_cast(get())` doesn't do that. – Jonathan Wakely Nov 29 '16 at 20:24