Semantics

Started by
15 comments, last by Hodgman 12 years, 5 months ago
I think that an important thing to accomplish in any API is to separate the interface from the implementation. One way of doing this is through the pimpl idiom. Another way is through the use of Interfaces, which are essentially Pure Abstract Classes. I like this approach a lot, because it allows for late binding and completely hides implementation (keep those public header files light and devoid of implementation details).

Example:


//Window.h - part of the public API

class IWindow
{
public:
static IWindow* New(int width, int height);
virtual ~IWindow() {}
virtual void setSize(int width, int height) = 0;
virtual void setTitle(const std::string& title) = 0;
}; //notice no implementation details here

//MyWindowImpl.h - private header

#include "Window.h"
class MyWindowImpl : public IWindow
{
public:
MyWindowImpl(int width, int height);
void setSize(int width, int height);
void setTitle(const std::string& title);
};

//Window.cpp - code file, not a part of the public API

#include "Window.h"
#include "MyWindowImpl.h"

IWindow* IWindow::New(int width, int height)
{
//we could dynamically choose which type of window to return
return new MyWindowImpl(int width, int height);
}

//example usage

#include "Widnow.h"

int main()
{
IWindow* wnd = IWindow::New(640, 480);
wnd->setTitle("Hello, World!");
delete wnd;
}


That's great, because the only thing the user need be exposed to is the interface. However, the one disadvantage is that you are stuck with pointers. I think most modern C++ programmers try to avoid pointers as much as possible. The STL for example, is an API mostly devoid of pointers. We don't create pointers to string or stream objects after all. I think most programmers expect a good modern API to have value semantics similar to:


int main()
{
Window wnd(640, 480);
wnd.setTitle("Hello, World!");
}


They want to be able to create their objects on the stack and benefit from RAII. Wouldn't it be nice if we could have our cake and eat it too? One solution would be to use smart pointers. We could do this instead:


//Window.h
class IWindow
{
public:
typedef std::shared_ptr<IWindow> Ptr;
...
}

//example

int main()
{
IWindow::Ptr wnd(IWindow::New(640, 480));
wnd->setTitle("Hello, World!");
}


You still have pointer semantics, but at least you don't have to worry about managing it's lifetime. However, I was working on a slightly different approach which may bring the it a bit closer to value semantics.


//Window.h

#include "SmartRef.h"

class IWindow
{
public:
virtual ~IWindow() {}
virtual void setSize(int width, int height) = 0;
virtual void setTitle(const std::string& title) = 0;
}; //very clean interface definition

typedef SmartRef<IWindow, int, int> Window;


//SmartRef.h

template <class T, class... InitArgs>
class SmartRef
{
public:
typedef SmartRef<T, InitArgs...> ref_type;
static ref_type New(InitArgs...) {}
SmartRef(InitArgs... args)
{
*this = ref_type::New(args...);
}
SmartRef(T* ptr)
: _ptr(ptr)
{}
T* operator->() { return _ptr.get(); }
private:
std::shared_ptr<T> _ptr;
};


//Window.cpp

#include "MyWindowImpl.h"

template<>
Window Window::New(int width, int height)
{
return new MyWindowImpl(width, height);
}


//example usage

#include "Window.h"

int main()
{
Window wnd(640, 480); //alternatively Window wnd = Window::New(640, 480);
wnd->setTitle("Hello, World!");
}


You are still stuck using the pointer operator, but everything else about it is like value semantics. These semantics look similar to Google's V8 API.

What exactly are your thoughts?
Advertisement
It is generally inappropriate for a library writer to force smart pointer semantics on consumers of a public library. It's more inappropriate to obscure that via typedef.

I think that an important thing to accomplish in any API is to separate the interface from the implementation. One way of doing this is through the pimpl idiom. Another way is through the use of Interfaces, which are essentially Pure Abstract Classes. I like this approach a lot, because it allows for late binding and completely hides implementation (keep those public header files light and devoid of implementation details).

Example:


//Window.h - part of the public API

class IWindow
{
public:
static IWindow* New(int width, int height);
virtual ~IWindow() {}
virtual void setSize(int width, int height) = 0;
virtual void setTitle(const std::string& title) = 0;
}; //notice no implementation details here

//MyWindowImpl.h - private header

#include "Window.h"
class MyWindowImpl : public IWindow
{
public:
MyWindowImpl(int width, int height);
void setSize(int width, int height);
void setTitle(const std::string& title);
};

//Window.cpp - code file, not a part of the public API

#include "Window.h"
#include "MyWindowImpl.h"

IWindow* IWindow::New(int width, int height)
{
//we could dynamically choose which type of window to return
return new MyWindowImpl(int width, int height);
}

//example usage

#include "Widnow.h"

int main()
{
IWindow* wnd = IWindow::New(640, 480);
wnd->setTitle("Hello, World!");
delete wnd;
}


That's great, because the only thing the user need be exposed to is the interface. However, the one disadvantage is that you are stuck with pointers. I think most modern C++ programmers try to avoid pointers as much as possible. The STL for example, is an API mostly devoid of pointers. We don't create pointers to string or stream objects after all. I think most programmers expect a good modern API to have value semantics similar to:


int main()
{
Window wnd(640, 480);
wnd.setTitle("Hello, World!");
}


They want to be able to create their objects on the stack and benefit from RAII. Wouldn't it be nice if we could have our cake and eat it too? One solution would be to use smart pointers. We could do this instead:


//Window.h
class IWindow
{
public:
typedef std::shared_ptr<IWindow> Ptr;
...
}

//example

int main()
{
IWindow::Ptr wnd(IWindow::New(640, 480));
wnd->setTitle("Hello, World!");
}


You still have pointer semantics, but at least you don't have to worry about managing it's lifetime. However, I was working on a slightly different approach which may bring the it a bit closer to value semantics.


//Window.h

#include "SmartRef.h"

class IWindow
{
public:
virtual ~IWindow() {}
virtual void setSize(int width, int height) = 0;
virtual void setTitle(const std::string& title) = 0;
}; //very clean interface definition

typedef SmartRef<IWindow, int, int> Window;


//SmartRef.h

template <class T, class... InitArgs>
class SmartRef
{
public:
typedef SmartRef<T, InitArgs...> ref_type;
static ref_type New(InitArgs...) {}
SmartRef(InitArgs... args)
{
*this = ref_type::New(args...);
}
SmartRef(T* ptr)
: _ptr(ptr)
{}
T* operator->() { return _ptr.get(); }
private:
std::shared_ptr<T> _ptr;
};


//Window.cpp

#include "MyWindowImpl.h"

template<>
Window Window::New(int width, int height)
{
return new MyWindowImpl(width, height);
}


//example usage

#include "Window.h"

int main()
{
Window wnd(640, 480); //alternatively Window wnd = Window::New(640, 480);
wnd->setTitle("Hello, World!");
}


You are still stuck using the pointer operator, but everything else about it is like value semantics. These semantics look similar to Google's V8 API.

What exactly are your thoughts?

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

There is nothing wrong with pointer use for polymorfic types it often makes it clearer that the type could be a derived type instead of the base class.

Also use a factory to create these class and the user neednt even see the implementations of the concrete types.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

You could create the library using simple abstract classes, and provide a wrapper that adds smart_pointer semantics. That way users can still choose to use the raw-pointer-interface.
Virtual interfaces irk me when they're actually not required. Reference-counted pointers also irk me when there's one logical owner (if it's acting like a value on [font="'Courier New"]main[/font]'s stack, it should be destructed when [font="'Courier New"]main[/font] unrolls - no need to ref-count in such a case).
My preferred re-write, as food-for-thought:[source lang=cpp]//Window.h
class Window
{
public:
static Window* New(int width, int height);
static void Delete(Window*);
void setSize(int width, int height);
void setTitle(const std::string& title);
private:
friend class MyWindowImpl;
Window(){}
~Window(){}
Window(const Window&);
Window& operator=(const Window&);
};


//MyWindowImpl.h - private header, not a part of the public API
#include "Window.h"
class MyWindowImpl : public Window
{
public:
MyWindowImpl(int width, int height);
void setSize(int width, int height);
void setTitle(const std::string& title);
};


//Window.cpp - code file, not a part of the public API
#include "MyWindowImpl.h"

Window* Window::New(int width, int height)
{
return new MyWindowImpl(int width, int height);
}
void Window::Delete(Window* p)
{
delete (MyWindowImpl*)p;
}

MyWindowImpl& Cast(Window* p) { return *(MyWindowImpl*)p; }
void Window::setSize(int width, int height) { Cast(this).setSize(width, height); }
void Window::setTitle(const std::string& title) { Cast(this).setTitle(title); }


//MyPointerHelpers.h
tempalte<class T>
class AutoPointer
{
public:
AutoPointer() : m_ptr() {}
~AutoPointer() { T::Delete(m_ptr); }
//todo - the usual constructors and operators here
private:
T* m_ptr;
};


//example
int main()
{//choice of pointer type is not coupled with the Window class. In this case, I'll use a single-owner RAII pointer.
AutoPointer<Window> wnd = Window::New(640, 480);
wnd->setTitle("Hello, World!");
}[/source]Also, if I was writing this in my own engine, I'd pass an allocator into [font="Courier New"]Window::New[/font] instead of using [font="Courier New"]new[/font]/[font="Courier New"]delete[/font], so that the allocation region isn't as coupled with the Window class (e.g. allowing it to actually be allocated on [font="'Courier New"]main[/font]'s stack like a regular local value).
When exactly would you consider virtual interfaces not required?

As far as reference counting goes, you could just as easily use std::unique_ptr instead if it is only logical for one owner.


friend class MyWindowImpl;


Here you are exposing the implementation in the public header, and what if you need more than one? (as you likely would with a Window class implementation, eg. Win32WindowImpl, X11WindowImpl, CocoaWindowImpl, etc)

You are still stuck using the pointer operator, but everything else about it is like value semantics.

That looks like it's still pointer semantics to me. When you copy your class it does a shallow copy, not a deep copy.

Personally, I'd avoid being overly clever in a library interface. If you ever stick this API in its own DLL then use of templates alone is going to be a major headache. Use of a abstract base class as an interface is well understood with exactly what the semantics are and leaves the user able to use whatever smart pointer they are most comfortable with.
How about this alternative approach?


class Window
{
public:
static Window* New(int width, int height);
virtual ~Window() {}
virtual void setSize(int width, int height) = 0;
virtual void setTitle(const std::string& title) = 0;
};



template <class T>
class Handle : public std::shared_ptr<T>
{
public:
template <class... Args>
Handle(Args... args)
: std::shared_ptr<T>(T::New(args...))
{}
};



int main()
{
Handle<Window> wnd(640, 480);
wnd->setTitle("Hello, World");
}


I guess the intent here is not so much to have value semantics per se, but rather to simply have a little more sugar than simply using shared_ptr by itself.
There doesn't seem to be much point. You still have a template dependence and are forcing a particular shared pointer implementation on the user just for being able to shave a few characters off pointer creation. Not to mention the fact that if you want shared_ptr and your Handle to coexist happily you'll need to add quite a bit more scaffolding than just the code you've posted.

This topic is closed to new replies.

Advertisement