Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Static member variable of class member instantiated twice

I have a template class, which has two static member variables, one int and another an std::array<volatile uint_fast32_t, 8>. When I instantiate the template with two different classes (which are templates themselves) as template parameters, for one of the instantiations everything works perfectly, i.e. there is exactly one copy of both variables. However, for the other, the array appears in duplicate in the symbol table, and indeed my code has a bug that when I set a value in the array in one compilation unit, the change does not appear in another.

This is for an embedded system, which is the reason for this weird idiom of using static templates for a kind of compile-time polymorphism.

In code: Header declaring the class itself

//dacmux.h

namespace HAL {

template<typename dac_write_sequence_t,
    unsigned int chans,
    typename sample_t = uint_fast32_t>
struct dacmux {
private:

    typedef std::array<volatile sample_t, chans> chans_t;
    static chans_t channels;

    static unsigned int nextchan;
...
};

//The static variables defined here,
//count on the compiler/linker to make sure
//there is exactly one definition
template<typename dac_write_sequence_t,
    unsigned int chans,
    typename sample_t> 
   typename dacmux<dac_write_sequence_t, chans, sample_t>::chans_t dacmux<dac_write_sequence_t, chans, sample_t>::channels{0};

template<typename dac_write_sequence_t, unsigned int chans, typename sample_t> 
    unsigned int dacmux<dac_write_sequence_t, chans, sample_t>::nextchan = 0;

template<typename dac_t, typename addr_t, typename en_t>
struct muxed_setter {
    ...
};

template<typename dac_t>
struct dac_setter {
    ...
};

}//namespace HAL

A header which distributes definitions of the hardware:

//Hardware_types.h
...
//Multiplexer for the internal DAC
typedef HAL::dacmux<HAL::muxed_setter<dac1, mux1_addr, mux1_en>, 8> mux1;

//Sequencer for writing the external DAC values
typedef HAL::dacmux<HAL::dac_setter<extdac1>, 8> extdac_sequencer;
...

The header Hardware_types.h is included in two source files, main.cpp, DebugConsole.cpp, both of which use both mux1 and extdac_sequencer.

As for as I understand, based on answers such as this one and many others, the compiler should take care that each of the static member variables is instantiated exactly once for each instantiation of the template?

However, when I set the values of extdac_sequencer::channels in DebugConsole.cpp, the changes are not reflected in an interrupt handler declared in main.cpp. The same works perfectly for mux1::channels. Indeed, an excerpt from the symbol table, extracted from the .elf by objdump -t:

20000280 l     O .bss   00000004 _ZN3HAL6dacmuxINS_10dac_setterINS_6ti_dacINS_3SPIINS_5SPI_2EEEN5GPIOs5pin_tINS6_1BELj12EEENS_12_GLOBAL__N_110xx68_frameENSA_12command_xx68ENSA_12channel_xx68EEEEELj8EjE8nextchanE
...
20000254 l     O .bss   00000020 _ZN3HAL6dacmuxINS_10dac_setterINS_6ti_dacINS_3SPIINS_5SPI_2EEEN5GPIOs5pin_tINS6_1BELj12EEENS_12_GLOBAL__N_110xx68_frameENSA_12command_xx68ENSA_12channel_xx68EEEEELj8EjE8channelsE
...
20000288 l     O .bss   00000020 _ZN3HAL6dacmuxINS_10dac_setterINS_6ti_dacINS_3SPIINS_5SPI_2EEEN5GPIOs5pin_tINS6_1BELj12EEENS_12_GLOBAL__N_110xx68_frameENSA_12command_xx68ENSA_12channel_xx68EEEEELj8EjE8channelsE
...
20000234  w    O .bss   00000020 _ZN3HAL6dacmuxINS_12muxed_setterIN4DACs11DAC_channelILj1EN5GPIOs5pin_tINS4_1AELj4EEEEENS4_12bit_stripe_tINS4_1CELj6ELj3EEENS5_ISA_Lj9EEEEELj8EjE8channelsE
...
2000027c  w    O .bss   00000004 _ZN3HAL6dacmuxINS_12muxed_setterIN4DACs11DAC_channelILj1EN5GPIOs5pin_tINS4_1AELj4EEEEENS4_12bit_stripe_tINS4_1CELj6ELj3EEENS5_ISA_Lj9EEEEELj8EjE8nextchanE

So the nextchan variable appears once per instantiation, as it should, and for mux1 so does channels. However, for extdac_sequencer, the channels variable is repeated, which I believe explains the bug.

Am I doing something wrong, or is this a compiler or linker bug?

Compiler: GCC arm-none-eabi 5.2.1 20151202

Linker: arm-none-eabi-ld 2.25.90.20151217

Linker options: -T mem.ld -T libs.ld -T sections.ld -nostartfiles -Xlinker --gc-sections -L"../ldscripts" -Wl,-Map,"Synth1Firmware.map" -Xlinker --cref --specs=nano.specs

Update:

I've narrowed down the conditions for this to happen:

If the first template parameter of dacmux is not itself a template, everything works, i.e. no duplicate symbol:

struct extdac1_setter {
    template<typename sample_t>
    inline static void update(sample_t val, unsigned int addr) {
        extdac1::write_and_update(val, addr);
    }
};

//Multiplexer for external DAC, works
typedef HAL::dacmux<extdac1_setter, 8> extdac_sequencer;

However, if the template parameter is templated itself, I get the duplicate symbol problem:

template<typename dac_t>
struct dac_setter {
    template<typename sample_t>
    inline static void update(sample_t val, unsigned int addr) {
        dac_t::write_and_update(val, addr);
    }
};

//Multiplexer for external DAC, this produces a duplicate symbol
typedef HAL::dacmux<dac_setter<extdac1>, 8> extdac_sequencer;

Here, extdac1 is itself, again, a template:

typedef HAL::DAC8568<dacspi, typename dacspi::nss> extdac1;

...and dacspi is a template, and so on. Also, in the case which does work, with the other instantation, while dac_write_sequence_t is a template, it isn't anymore a template of templates. So I'm starting to think that this is a problem with template recursion depth, i.e. ld isn't looking deep enough.

A further interesting observation: on exactly the same condition as having the duplicate symbol, the Eclipse syntax highlighter says "invalid template parameters" on the line declaring extdac_sequencer, although the actual compilation step goes through.

like image 884
Timo Avatar asked May 29 '16 13:05

Timo


1 Answers

Turns out this was my sillyness: I was using an unnamed namespace in a header that defines the template HAL::DAC8568, which is defined as

template<typename spi_t, typename nss_t> using DAC8568 = ti_dac<spi_t, nss_t,
                        xx68_frame,
                        command_xx68,
                        channel_xx68>;

Here xx68_frame, command_xx68, and channel_xx68 are all defined in the unnamed namespace (which is of course the wrong thing to do in a header). This of course means that when instantiated from a different compilation unit, I get a different type for each of them and therefore a different type for DAC8568 and so on, so it's perfectly natural to get another instance of the static variable.

Changing the unnamed namespace to namespace detail fixed the problem immediately.

I am still somewhat baffled by the fact that the mangled names in the linker output seem identical. How can that be?

Anyway, we learn from this the following (some of which we knew already):

  1. I'm an instantiation of the simpleton -pattern
  2. unnamed namespaces in headers are truly evil
  3. bugs stemming from the above can be quite subtle
like image 131
Timo Avatar answered Sep 20 '22 14:09

Timo