Updating from enum to enum class causes issues

I have a small, in-progress, project specifically chosen to let me explore and learn C++. In this project, I had declared an enum in a global.h file, which consisted of, mostly, static const ints. global.h was included in a number of other compilation units and everything worked.

I decided to update the enum to an enum class. I missed some of the consequences (no implicit conversion to int, for example) but I worked through those. I even added an operator++() overload to the enum class, which I inserted in the global.h file.

I got "multiple declarations of operator++()" linker errors but I figured out why: It was finding the definition in each of the .obj files. Adding "inline" to the definition solved the problem.

What I don't understand is why?

I suppose I could have created a global.cpp and moved the operator++() definition there but that seemed to defeat the purpose of setting the global values aside in a .h.

Is there a better way?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum class kWxData
{
	Temperature,
	WindDir,
	WindStr,
	Cloud,
	Precip,
	END_OF_ENUM
};

inline kWxData& operator++(kWxData& data) {
	switch (data) {
	case kWxData::Temperature:
		return data = kWxData::WindDir;
	case kWxData::WindDir:
		return data = kWxData::WindStr;
	case kWxData::WindStr:
		return data = kWxData::Cloud;
	case kWxData::Cloud:
		return data = kWxData::Precip;
	case kWxData::Precip:
		return data = kWxData::Temperature;
	default:
		throw std::out_of_range("Invalid kWxData value");
	}
}
Last edited on
The operator++() is just a function like any other, and if a function definition appears in multiple translation units then it needs to be inline, otherwise the linker doesn't know how to resolve the multiple definitions issue.

By the way, you can make your implementation shorter:
1
2
3
4
5
6
7
#include <type_traits>

inline kWxData& operator++(kWxData& data){
    typedef typename std::underlying_type<kWxData>::type T;
    data = ((T)data + 1) % (T)kWxData::END_OF_ENUM;
    return data;
}
Last edited on
helios, that implementation wouldn't throw, right? It's possible the user relies on Precip++ throwing.

(Edit: I meant ++Precip, as seeplus mentions below.)
Last edited on
If the input has a value > kWxData::END_OF_ENUM the program has undefined behavior anyway. The default case does make sense in case new values get added to the enum, to let the maintainer know to update the implementation, however the modulo operation sidesteps that issue entirely and doesn't need updating.
helios wrote:
If the input has a value > kWxData::END_OF_ENUM the program has undefined behavior anyway.

I don't mind treating it as "UB" but technically enums are usually allowed to have values other than the listed enumerators.

https://eel.is/c++draft/dcl.enum#8.sentence-1
For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type.

An "enum class" always has a fixed underlying type that defaults to int. That means you could legally, as far as the C++ standard is concerned, store any valid int value in a kWxData enum.
Last edited on
Note that this implementation is for prefix increment (eg ++data). It won't work for postfix increment (eg data++) which needs to be implemented separately. eg.

1
2
3
4
5
6
7
8
9
10
#include <type_traits>

// Postfix increment
inline kWxData& operator++(kWxData& data, int) {
    const auto org {data};

    typedef typename std::underlying_type<kWxData>::type T;
    data = ((T)data + 1) % (T)kWxData::END_OF_ENUM;
    return org;
}


This requirement to copy the original data to return is why prefix increment should be used where possible instead of postfix.

Thanks all for giving me something else to think about.

https://cplusplus.com/forum/general/285537/#msg1240465
I get that inline works, but why does it work? Is it because inline drops the code in wherever it occurs and no operator++ is actually called?
The keyword "inline" is misleading in this regard.

Is it because inline drops the code in wherever it occurs and no operator++ is actually called?
Not necessarily. The C++ keyword "inline" doesn't actually force inlining, it just means you are allowed to have multiple definitions of the same function.

So now, A.cpp can define the operator++ function, and B.cpp can also define the same function (either from copy-paste* or #include).

*But under no circumstances would I suggest actually copy-pasting the non-static inline function into multiple files because if those two inline functions diverge, you get undefined behavior because the compiler is allowed to assume both functions have the same address.
Last edited on
Topic archived. No new replies allowed.