The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal Public Domain Dedication

Namespace vs struct

Notes published the
8 - 10 minutes to read, 2005 words

A controversial comparison, but it happened to me more than once.

What is a namespace and what is a struct

A namespace provides a method for preventing name conflicts in large projects. Symbols declared inside a namespace block are placed in a named scope that prevents them from being mistaken for identically-named symbols in other scopes.

Classes and structs are user-defined types.

Nevertheless struct can be used similarly to namespaces: by defining all functions as static.

Notice that every argument said for struct also holds for class.

The problem

Some projects, mostly those with an older codebase and heavily influenced by Java conventions, tend to use struct and/or class with static member functions instead of namespaces (or together with namespaces, with a policy that everything needs to be in a class).

As normally namespaces are a superior solution, it might seem a no-brainer to replace one with another. As long as those structs have been only used for providing a namespace, it is worth knowing the differences and similarities.

There is, as far as I know, one uncommon use case where a struct is better than a namespace.

Feature comparison

Nesting

While nesting namespaces/structs is not (or should not be) that common, it can be useful

namespace ns1 {
	namespace ns1 {
		void foo();
	}
}

or, since C++17

namespace ns1::ns2 {
    void foo();
}

Similarly, structures can be nested too

struct s1 {
	struct s2{
		static void foo();
	};
};

While it is true that extensively nesting namespaces 🗄️ should be discouraged, the same issue applies to structures. A namespace (achieved through namespace or struct) is for avoiding collisions, it should not be used for disambiguating API.

Splitting over multiple files

A namespace can be split into the same file or over multiple files

//file a.hpp
namespace ns {
	void foo();
}
//file b.hpp
namespace ns {
	void bar();
}

This feature is extremely useful when maintaining code.

Being able to split and move functions between files is useful for cleaning and reorganizing, but if one needs to adapt all users that include a specified file at once, it can become a daunting task.

For example, consider you have a header file that defines some algorithms

//file algorithm.hpp
namespace ns {
	void sort(std::span<int>);
	void binary_sort(std::span<int>);
}

over time, overloads for existing algorithms, specialization, or other algorithms get added, and the header file grows considerably in size. It would be useful to split it up, and group together the algorithms that solve a similar issue.

//file algorithm.hpp
#include <algorithm/sort.hpp>
#include <algorithm/binary_sort.hpp>
// ...

//file algorithm/sort.hpp
namespace n2 {
	void sort(std::span<int>);
}

//and so on

Another advantage is that while algorithm.hpp stays available for code that already uses it, a user can also choose to include algorithm/sort.hpp directly to decrease build times/reduce dependencies.

If we would have used a structure instead of a namespace

//file algorithm.hpp
struct s {
	static void sort(std::span<int>);
	static void binary_sort(std::span<int>);
};

such change would not be possible.

ADL

Argument-dependent lookup permits calling other functions from the same namespace without qualifying.

I think a nice example of how ADL can increase readability is provided by mutexed_obj

#include <mutex>

namespace fek {
template<class T, class mutex = std::mutex>
class mutexed_obj {
		T obj;
		mutable mutex m;
	public:
		template<typename... Args>
		explicit mutexed_obj(Args&&... args) : obj(std::forward<Args>(args)...) {}

		template <typename... Objs, class F>
		friend decltype(auto) lock(F f, Objs&&... objs);
};

template <typename... Objs, class F>
decltype(auto) lock(F f, Objs&&... objs){
	std::scoped_lock lck(objs.m...);
	return f(objs.obj...);
}
}

int main(){
	fek::mutexed_obj<int, std::mutex> mo1(1);
	fek::mutexed_obj<int, std::recursive_mutex> mo2(2);

	auto v1 = lock([](int&, int&){ return 1;}, mo2, mo1);
	auto v2 = lock([](const int&, int&){return 1;}, mo2, mo1);
	auto v3 = lock([](const int&){return 1;}, mo2);
}

Notice that in the main function, which is outside of namespace fek, we can write auto v1 = lock([](int&, int&){ return 1;}, mo2, mo1); instead of auto v1 = fek::lock([](int&, int&){ return 1;}, mo2, mo1);.

Granted, in this case, spelling fek:: is not a big deal, those are just five characters. But if such a facility is buried in companyname::core, then prefixing the namespace to every function is obfuscating the code.

Thus ADL can lead to easier-to-read code, especially in the case of a namespace with a long name), can be used for providing customization points (for example for swap), and makes std::operator<< for stream just work™ (it is the reason why ADL has been introduced.).

ADL also has disadvantages, the main issue is that it makes it possible to call the wrong function, ie the function of another namespace. Common names, like size, are problematic, this piece of code will compile until C++14, and fail from C++17 because of ADL:

#include <vector>

template<class T> int size(const T& x) {
 	return x.size();
}

void foo(){
	std::vector<int> v;
	auto i = size(v);
}

or similarly

#include <vector>

namespace ns {
template<class T> int size(const T& x) {
	return x.size();
}
}

void foo(){
	using namespace ns;
	std::vector<int> v;
	auto i = size(v);
}

How to disable ADL

In some cases, it is desirable to disable ADL. The easiest way to do it is by using a function object.

namespace ns{
struct foo{};

int bar(foo);

struct bar2 {
	int operator()(foo) const;
} bar2;

}

int main(){
	auto f = fek::foo();
	ns::bar(f);
	bar(f); // works because of ADL
	ns::bar2(f);
	//bar2(f); // ADL does not apply, does not compile
}

// in .cpp file
namespace ns {
	int bar(foo){return 1;}
	int bar2::operator()(foo) const {return 2;}
}

dead code

A structure has a constructor, destructor, copy-move operations, etc.etc., unless those are deleted.

It is not a novel error to instantiate a class only for calling a static member function.

A namespace does not have any of those.

While it is not a dealbreaker, it makes the code less clear. As the class is instantiated, obviously someone did not realize that he was calling a stateless function.

To avoid those errors, it is possible to delete all those operations generated by the compiler explicitly. It is not much code, but still, we are opting out of all features we have explicitly requested. Seems like a contradiction.

Private implementation details

A struct permits to put some private functions in header files, for example

//file algorithm.hpp
struct s {
		static void sort(std::span<int>);
	private:
		static void sort_impl(std::span<int>);
};

with namespace, there is no such feature. One can establish a convention, like creating a namespace detail one the same header file

//file algorithm.hpp
namespace detail {
	static void sort_impl(std::span<int>);// used by sort, not part of the API
}
namespace fek{
	static void sort(std::span<int>);
}

or in a separate header

// algorithm/detail.hpp
namespace detail {
	static void sort_impl(std::span<int>);// used by sort, not part of the API
}

// algorithm.hpp
#include <algorithm/detail.hpp> // used by sort, not part of the API

namespace fek{
	static void sort(std::span<int>);
}

In this case, it would be better to declare void sort_impl(std::span<int>) in the implementation file, or in some other header file and not include it where void sort(std::span<int>); is defined. Thus remove the implementation details from the header file.

On the other hand, this is not always possible. If both sort and sort_impl are templated functions, it might be necessary to put them both in the header file.

export (dllexport) only a subset of an API

In the case of a struct, either the whole struct is exported, or none of it. It is not possible to export only some member functions.

In the case of a namespace, all functions can be exported independently, thus it is possible to export only a subset.

AFAIK it is not possible to export everything at once with a namespace if one wants to.

Inject namespaces in templated functions

Consider you have something like

template <class T, class ns>
void foo(cons T& t){
	ns::bar(t);
}

In this case, ns cannot be a namespace (thus it is not possible, for example, to switch from std::sort to acme::sort).

As this use case is pretty rare (passing namespaces as template parameters has been considered as an addition to the language), it should not be a reason for preferring a struct over a namespace. Also, with another layer of indirection, it is possible to easily get the wanted functionality.

First of all, it is unlikely that foo needs to access a lot of functions from a given namespace, thus it is possible to create a helper struct that provides the necessary interface.

For simple cases, it is nearly possible to automate the job without writing any additional function

namespace ns1 {
	void bar(int);
	// and many other functions
}
namespace ns2 {
	void bar(int);
	// and many other functions
}

struct s1 {
	constexpr static decltype(ns1::bar)* bar = ns1::bar;
};
struct s2 {
	constexpr static decltype(ns2::bar)* bar = ns2::bar;
};

template <class ns>
void foo(int i){
	ns::bar(i);
}

int main(){
	foo<s1>(42);
	foo<s2>(42);
}

With the help of a macro, it is possible to remove most of the boilerplate

#include <type_traits>

namespace ns1 {
	void bar(int);
	// and many other functions
}
namespace ns2 {
	void bar(int);
	// and many other functions
}

#define TO_STATIC_MEM_FUN(ns, fun) \
	static_assert(std::is_function<decltype(ns::fun)>::value, "not a function"); \
	constexpr static decltype(ns::fun)* fun = ns::fun

struct s1 {
	TO_STATIC_MEM_FUN(ns1, bar);
};
struct s2 {
	TO_STATIC_MEM_FUN(ns2, bar);
};

template <class ns>
void foo(int i){
	ns::bar(i);
}

int main(){
	foo<s1>(42);
	foo<s2>(42);
}

For more complex cases (especially function overloading), it might be necessary to create such structures manually.

importing names

With namespaces, it is possible to import names from other namespaces in the current one.

This makes it possible to use shorter names (in a similar way to ADL), as we do not need to specify the whole namespace, or provide aliases, like in the following example:

void foo();

namespace ns1 {
	void bar();
}

namespace ns2 {
	using ::foo;
	using ns1::bar;
	// using namespace ns1; // would make all symbols of ns1 visible in ns2
}

int main() {
	ns2::foo(); // calls foo
	ns2::bar(); // calls ns1::bar
}

no shared state (by default)

This is a moot point, but for me, it’s obvious that free functions do not have a shared state unless stated otherwise, because mutable globals are bad.

While static member variables are globals too, they are often not felt as such. Thus it is easier to let them sneak in.

While both with namespaces and structs it is the same effort to make them stateless, in my experience, free functions have a higher rate of success in remaining stateless.

Conclusions

To sum up the important parts:

namespace struct

can be nested

yes

yes

can be split over multiple files

yes

no

ADL

yes

no

unnecessary/dead code

no

yes

put private implementation details in the header

no

yes

export only a subset of a file

yes

no

inject in templated functions

no

yes

importing names (using declaration)

yes

no

The main advantage of using a struct is being able to put implementation details in the header file without making those accessible. For everything else, namespace does the job it has been designed for in a better way.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.