Jump to content

  • Log In with Google      Sign In   
  • Create Account


Semantics


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
16 replies to this topic

#1 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 30 October 2011 - 07:23 PM

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?

Sponsor:

#2 Telastyn   Crossbones+   -  Reputation: 3718

Like
3Likes
Like

Posted 31 October 2011 - 08:07 AM

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.

#3 NightCreature83   Crossbones+   -  Reputation: 2671

Like
0Likes
Like

Posted 31 October 2011 - 08:18 AM

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, Mad Max

#4 NightCreature83   Crossbones+   -  Reputation: 2671

Like
0Likes
Like

Posted 31 October 2011 - 08:20 AM

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, Mad Max

#5 Koen   Members   -  Reputation: 454

Like
0Likes
Like

Posted 31 October 2011 - 08:23 AM

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.

#6 Hodgman   Moderators   -  Reputation: 27618

Like
0Likes
Like

Posted 31 October 2011 - 08:32 AM

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 main's stack, it should be destructed when main unrolls - no need to ref-count in such a case).
My preferred re-write, as food-for-thought:[source lang=cpp]//Window.hclass 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.htempalte<class T>class AutoPointer{public: AutoPointer() : m_ptr() {} ~AutoPointer() { T::Delete(m_ptr); } //todo - the usual constructors and operators hereprivate: T* m_ptr;};//exampleint 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 Window::New instead of using new/delete, so that the allocation region isn't as coupled with the Window class (e.g. allowing it to actually be allocated on main's stack like a regular local value).

#7 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 31 October 2011 - 09:13 AM

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)

#8 SiCrane   Moderators   -  Reputation: 9387

Like
0Likes
Like

Posted 31 October 2011 - 10:11 AM

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.

#9 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 31 October 2011 - 03:19 PM

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.

#10 SiCrane   Moderators   -  Reputation: 9387

Like
0Likes
Like

Posted 31 October 2011 - 03:27 PM

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.

#11 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 31 October 2011 - 03:37 PM

You still have a template dependence


What do you mean?

and are forcing a particular shared pointer implementation on the user


Is that really so bad? What's the issue here?

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.


What exactly is required?

#12 Cornstalks   Crossbones+   -  Reputation: 6966

Like
0Likes
Like

Posted 31 October 2011 - 04:06 PM

Question. Why would I prefer doing Handle<Window> myWindow(640, 480) over doing std::shared_ptr<Window> myWindow(Window::New(640, 480))? What's the big gain?
[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#13 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 31 October 2011 - 04:13 PM

Well I guess it's because I have yet to use any library which simply uses nothing but pointers and requires syntax like std::shared_ptr<Window> myWindow(Window::New(640, 480)). The ones I've seen tend to have neat and not overly verbose syntax.

#14 ApochPiQ   Moderators   -  Reputation: 14281

Like
0Likes
Like

Posted 31 October 2011 - 04:32 PM

What's "overly verbose" or "not neat" about that format for instantiating a window?

#15 SiCrane   Moderators   -  Reputation: 9387

Like
0Likes
Like

Posted 31 October 2011 - 04:34 PM


You still have a template dependence

What do you mean?

You have an interface that depends on template data types. Since you say this is for a library then you've got some major bad juju going if you ever want to separate compilation of the library from the application that uses the library such as the previously mentioned DLLs.


and are forcing a particular shared pointer implementation on the user

Is that really so bad? What's the issue here?

It's not a big issue if yours is the only library that exists. However, many people resent needing to use five different smart pointers if they want to use five different libraries. And some organizations have their own smart pointer implementations they use in house or have otherwise standardized on a (set of) smart pointers. Note that you've already gotten one objection in this thread about introducing a reference counted smart pointer if someone only wants a single reference.

What exactly is required?

At the bare minimum you need to support construction of your Handle type from a shared_ptr. Assignment would probably be a good idea as well. And since you're using C++11 features, it wouldn't be a bad idea to put in the rvalue reference versions of those.

#16 Chris_F   Members   -  Reputation: 1938

Like
0Likes
Like

Posted 31 October 2011 - 04:53 PM

You have an interface that depends on template data types. Since you say this is for a library then you've got some major bad juju going if you ever want to separate compilation of the library from the application that uses the library such as the previously mentioned DLLs.


I don't write closed source code, so it's not so much of an issue. It's not a terrifically complex template, and I wouldn't nesisarrilty consider it a part of the API, just a convenience wrapper, so there is no need to hide anything and not simply make it header only. After all, std::shared_ptr itself is a header only template class. There is nothing stopping somebody from using Window* wnd = Window::New(640, 480) or std::shared_ptr<Window> wnd(Window::New(640, 480))

#17 Hodgman   Moderators   -  Reputation: 27618

Like
0Likes
Like

Posted 31 October 2011 - 07:49 PM

When exactly would you consider virtual interfaces not required?

In this case specifically, when there's only one implementation of the interface being compiled.
In other cases, there's always alternatives to virtual interfaces, and the alternatives often produce better code.

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

Yep, as long as you don't couple the pointer type with the window class, like in your rewrite.

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)

I exposed the name of the implementation - though I could have avoided that by making the default constructor/destructor protected instead of private if you find it neater.

In this case, you don't need more that one implementation of your Window interface at a time, because you're not going to have a particular build that supports both Win32 and X11, etc... Same goes for input APIs, rendering APIs, etc, etc...

I've been responsible for optimising middleware libraries that use interfaces in this manner, and it causes a whole lot of pain for no actual benefit (no link-time optimising possible, guaranteed branch-predict fails on each call, unavoidable load-hit-stores pushing/reading args from the stack each call, etc) -- though in this case, I'm sure it's not a big deal if your "Window" class is sub-optimal, as it's not really used that frequently ;)

It is generally inappropriate for a library writer to force smart pointer semantics on consumers of a public library.

Well I guess it's because I have yet to use any library which simply uses nothing but pointers and requires syntax like ... The ones I've seen tend to have neat and not overly verbose syntax.

I think there's a difference here between "small libraries" and "framework libraries".
Frameworks are generally (obnoxiously?Posted Image) an environment in which all further development must conform to (often game engines fall into this category). They often define their own smart-pointer types, define their own semantics, and demand that you fit in with their way of doing things.
If smaller libraries did the same thing, it would definitely be obnoxious, because you'd never be able to integrate several libraries together without writing tonnes of glue code.
Frameworks feel they're exempt from this situation, because they do so much stuff, and they do it well (in their opinionPosted Image).




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS