Overloading the assignment operator - fweiss/tm4c-led-pwm GitHub Wiki
Some people may object to
PortA.clockEnable = true;
This employs C++ operator overloading which is a well-known code smell.
However the assignment operator "=" is considerably different than arithmetic operators,
such as "+", "/".
The latter should have no side effects. For example when we see (where T generalizes a type, such as int):
T a = 1;
T b = 3;
T c = 4;
a = b + c;
We expect a
to be mutated and equal to 7, while b
and c
remain unchanged, unmutated.
The assignment operator "=" is different. It does mutate the LHS.
We can use the assignment operator to mutate bits as well. For example:
struct SomeRegister {
bool someBit : 1;
} reg;
reg.someBit = true;
We expect someBit
to now be true.
So now let's turn to using this for manipulating bits in MMIO registers. We will overload the assignment operator, "=", which will give the same effect as in the bitfield example above.
Here's the code (simplified for this illustration):
template<uint32_t registerAddress, uint8_t bitNumber>
struct RegisterBit {
void inline operator=(bool onoff) {
constexpr uint32_t bitmask = (1 << bitNumber);
if (onoff) {
*(volatile uint32_t*)registerAddress |= bitmask;
} else {
*(volatile uint32_t*)registerAddress &= ~ bitmask;
}
}
};
struct Port {
RegisterBit<0x40022380, 0> clockEnable;
};
Port portA;
portA.clockEnable = true;
Here we declare a struct template RegisterBit
.
It has two template parameters, registerAddress
and bitNumber
.
It has a member function operator=
which overloads the assignment operator "=".
That member function takes a single boolean parameter onoff
.
The body of the function defines a bitmask
variable using the shift operatore "<<" and the struct template's bitNumber
parameter.
That variable is declared constexpr
, so we expect the compiler to evaluate the shift expression at compile time.
The body also has a branch depending on the runtime method parameter onoff
.
The true branch uses the |=
operator to set the bit, while the false branch uses the idiomatic &= ~
expression to clear the bit.
The last line in the illustration shows the usage of this code for setting a bit in an MMIO register.
The portA
variable is an object that has a member variable clockEnable
.
That member variable is a struct template instance with the given register address and bit number.
The assingment operator "=" invokes the operator=
method, passing in the value true
.
The invocation of the operator=
method, called with a value of true, cause the following to be exacuted:
*(volatile uint32_t*)0x40022380 |= 0x00000001;
This gives the desired effect of setting bit number 0 in the MMIO register at address 0x40022380.
Objections
I started a discussion of this on reddit. What I heard was:
- Some alredy use C++ exclusicely for embedded projects
- some are in favor of using C++
- the usual crowd of C++ haters
- the stigma of operator overloading
- some questioned the semantic alignment of the assignemnt operator
- it helped when I disentangled the assignmet operator from operator overloading in general
There were many who prefered a procedural interface. For example portA.enableClock()
or Enable_Clock(portA)
.
I could concede this, but I still hold that portA.clockEnable = true
is more object oriented and direct.
Comparison of interfaces:
- Bare metal (CMSIS):
RCC->AHB1ENR |= (1 << 0);
- CMSIS+ (opencm3):
periph_clock_enable(RCC_GPIOA);
- STM32 HAL:
__HAL_RCC_GPIOA_CLK_ENABLE();
- My C++ version:
PortA.clockEnable = true;
I had an engaging chat with o4. At first it also objected to using the assingment operator that way. I think when I pointed out that the bitfield usage was the same, it softened up. It did conclude with the following caveats:
- it's not the semantics, it's audience expectations
- choose names that disambiguate that it is an MMIO access
- add
operator bool()
to clarify the intention - disable copy/move to avoid accidental misuse
I think it was actually this prompt of mine that cleared things up:
it's kind of weird to me how people interpret gpioAClock = true. they seem to assume that it's setting a bit in memory, which is actually true. it's just that they think it's ordinary memory, not MMIO. to me, the symbol "gpioAClock" very strongly implies this is related to MMIO, not regular memory
As a followup, I asked it if the there were non-MMIO cases where the assignment operation had side effects. It listed quite a few. It also noted that context was important to signel the presence of side effects.
I think the objections come from C programmers who are quite unaware of the C++ usages, such as std::unique_ptr. But even then, there are a few examples in C where a global variable changes the behavior of the program, such as reassigning stdin, etc.