Risoluzione automatica dello spazio dei nomi in C ++

6

La mia domanda è incentrata sulla progettazione e il codice incluso in questa domanda ha lo scopo di chiarire il problema che sto vivendo. Sono interessato alle risposte a livello concettuale e non al codice per rispondere alla mia domanda.

Sfondo:

Quando si programmano i sistemi bare metal, è spesso necessario manipolare i bit nel registro mappato in memoria controllando il comportamento delle periferiche on-chip o esterne. Di solito, ci sono librerie software che definiscono questi bit e registri. Un esempio minimo potrebbe essere simile a questo:

#include <stdio.h>
#include <stdint.h>

/* Might be in some library like this: */
#define SOME_PERIPHERAL_BASE_ADDRESS 0x8fce0000
#define SOME_PERIPHERAL_REG1 (*((uint16_t*)(SOME_PERIPHERAL_BASE_ADDRESS + 0x8e)))

#define SOME_PERIPHERAL_REG1_AM  (1 << 3)
#define SOME_PERIPHERAL_REG1_FOO (1 << 4)
#define SOME_PERIPHERAL_REG1_BAR (1 << 5)


/* User code: */
int main()
{
    /* How to use these definitions on a bare metal system: */
    /* SOME_PERIPHERAL_REG1 is initialies to the reset value by hardware, so
     * there is no need to initialise it. */
    /* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */
    /* This will of course segfault when not on a bare metal system. */
    /* SOME_PERIPHERAL_REG1 = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; */
    /* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */

    /* Ersatz dummy code for testing on a pc: */
    uint16_t value=0;
    printf("reg1 = %i\n", value);

    value = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; /* <- No type-safty! */

    printf("reg1 = %i\n", value);

    return 0;
}

Problemi con questo approccio:

Sebbene questo codice sia ragionevolmente leggibile e breve, manca in certi modi:

  • Un programmatore sarà noioso dover ridigitare il prefisso (SOME_PERIPHERAL_REG1_ nell'esempio) per ogni valore di registro
  • Nessuna sicurezza di tipo: un programmatore può facilmente impostare un registro su un valore destinato a un altro registro (ad esempio, impostare reg1 di Some_peripheral su un valore destinato al registro di controllo di Another_peripheral) o non su un valore di registro (ad es. imposta reg1 su 15 (un intero)).
  • Un altro problema con questo approccio è che alcuni registri non sono accessibili con assegnazioni semplici. Ad esempio, utilizzo un controller video SSD1289 collegato all'interfaccia di memoria esterna del mio microcontrollore. Per impostare i registri dell'SSD1289, dovrò prima scrivere l'indirizzo del registro su un determinato indirizzo nell'area di memoria seguito da una scrittura del nuovo valore di registro su un altro indirizzo in quella regione. Con la semplice C, non c'è modo di incapsulare questo tranne che usando una funzione setter.

Soluzione potenziale:

Per superare questo, ho provato a creare un oggetto registro in C ++ che è parametrizzato con un enum contenente le definizioni bit e un puntatore all'indirizzo del registro. Un esempio minimo potrebbe essere simile a questo:

#include <iostream>
#include "enum_binary_operators.hpp"

// Code that should go in the library:

/**
 * \brief An object ancapsulating a memory-mapped peripheral control register.
 *
 * \param reg_bits      An enum defining the bitfields for that register.
 * \param ret_address   The memory-mapped address where the register lies in
 *                      memory
 */
template < typename reg_bits, uintptr_t reg_address >
class Register
{
    private:
        // On the intended pare metal target, this wouldn't be neccesary, as
        // the value would be stored in the memory-mapped address of the register.
        uint16_t value = 0;
    public:
        // Write a new value to the register
        inline Register& operator= (reg_bits const& val)
        {
            // This is what is intended, but of course segfaults when not on bare metal.
            //*reinterpret_cast<uint16_t*>(reg_address) = static_cast < uint16_t > (val);

            // dummy replacement of the above line.
            value = static_cast < uint16_t > (val);
            // possibly do some more things like send the value to a remote register.
            return *this;
        }
        operator uint16_t()                         // Read register
        {
            // This is what is intended, but of course segfaults when not on bare metal.
            //return *reinterpret_cast<uint16_t*>(reg_address);

            // dummy replacement of the above line.
            return value;
        }
        [[deprecated("Please only use the appropriate register bit enum values to manipulate the register!")]] Register& operator= (int val) {}
};


/**
 * \brief An Object encapsulating a memory-mapped peripheral
 *
 * \param base_address  The base address of the registers belonging to that peripheral.
 */
template < uintptr_t base_address>
class Some_peripheral
{
    public:
        // This enum should not be in global namespace to avoid name conflicts.
        // For example many peripherals might have a register named
        // "control_register".
        enum reg1_bits : uint16_t
        {
            AM  = (1 << 3),
            FOO = (1 << 4),
            BAR = (1 << 5),
        };
        // more register bit definitions

        Register < reg1_bits, base_address + 0x8e > reg1;
        // more registers

    // ...
    // possibly more high-level functions for the peripheral like
    // initialisation routines, transmission routines, etc.
};

// More peripherals


// The base address should be provided by some file defining the peripheral addresses for a given chip.gg
//extern constexpr some_peripheral_base;
constexpr uintptr_t some_peripheral_base = 0x8fce0000;

Some_peripheral<some_peripheral_base> some_peripheral1;
// And some more peripherals





// User code:

int main()
{
    // Get the current value of the register.
    std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;



    // Each register can only be written with the bits that belong to it.
    some_peripheral1.reg1 = Some_peripheral<some_peripheral_base>::reg1_bits::AM
                          | Some_peripheral<some_peripheral_base>::reg1_bits::BAR;

    // This is the intended usage:
    // Somehow, operator= should be able to deduce the scope resolution automatically at runtime.
    //some_peripheral1.reg1 = AM | BAR;

    std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;

    return 0;
}

Con enum_binary_operators.hpp:

#ifndef ENUM_BINARY_OPERATORS_HPP
#define ENUM_BINARY_OPERATORS_HPP

#include <type_traits>


template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator|(T const& x, T const& y)       {
    return static_cast < T >   (static_cast < uint16_t > (x) | static_cast < uint16_t > (y));
}


template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator&(T const& x, T const& y)       {
    return static_cast < T >   (static_cast < uint16_t > (x) & static_cast < uint16_t > (y));
}


template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator^(T const& x, T const& y)       {
    return static_cast < T >   (static_cast < uint16_t > (x) ^ static_cast < uint16_t > (y));
}


template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(T const& x, T const& y)       {
    return static_cast < T >   (static_cast < uint16_t > (x) << static_cast < uint16_t > (y));
}


template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(unsigned int const x, T const& y)       {
    return static_cast < T >   (x << static_cast < uint16_t > (y));
}

template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(int const x, T const& y)       {
    return static_cast < T >   (x << static_cast < uint16_t > (y));
}



#endif /* ENUM_BINARY_OPERATORS_HPP */

Domande:

Ora le mie domande sono:

  • Sono corretto nel presupporre che questo dovrebbe arrivare a un tempo di esecuzione trascurabile? Ovviamente, il tempo di compilazione verrà aumentato.
  • C'è un modo migliore per farlo?
  • E c'è un modo per sbarazzarsi dei lunghi specificatori di scope nel compito? L'idea è qui, che il compilatore sa già, da dove i membri dell'enum dovranno venire (dato che l'enum è un argomento template per la classe register), quindi forse si potrebbe ottenere che includa la corretta risoluzione dell'ambito da sola in in qualche modo.
posta mox 28.10.2015 - 16:16
fonte

2 risposte

2

Una delle domande sollevate ha una risposta facile: ridigitare il prefisso.

Insegno che tali prefissi dovrebbero essere una parte sematic non solo una concatenazione con il nome: prefix::x , non prefix_x .

Per i flag, mettili nel loro spazio dei nomi annidato. Puoi use all'interno della funzione o anche l'ambito specifico in cui un gruppo di essi viene utilizzato insieme. (Questo è un caso in cui l'uso delle direttive è OK, perché è mirato e locale.)

{ // in some inner scope
    using namespace someperiphial::reg1;
    Blah blah foo, bar, am  // instead of somperiphial::reg1::foo etc.
}
    
risposta data 01.11.2015 - 02:09
fonte
1

Beh, ovviamente uint16_t è il tipo sbagliato e si voleva std::underlying_type<T> negli operatori binari. Sono piuttosto avidi, dopo tutto, e non puoi supporre che tutte le enumerazioni siano di 16 bit senza segno.

Un'alternativa per renderli meno avidi è mettere le tue enumerazioni e questi sovraccarichi nel loro spazio dei nomi dedicato. La ricerca dipendente dall'argomento includerà solo questi sovraccarichi quando applicati alle tue enumerazioni.

Per quanto riguarda la questione dell'efficienza, praticamente tutto ciò che il compilatore fa è gratuito in fase di runtime. Ancora meglio, a causa delle regole di tipo aliasing, i costrutti C ++ tipizzati sono talvolta ancora più efficienti. Un uint16_t* non può puntare alle tue enumerazioni, il che riduce i rischi di aliasing.

Il problema con

some_peripheral1.reg1 =
       some_peripheral<some_peripheral_base>::reg1_bits::AM |
       some_peripheral<some_peripheral_base>::reg1_bits::BAR;

è che l'albero di analisi C ++ viene analizzato dal basso verso l'alto. In parole povere, C ++ vede A = B | C che sono due chiamate operatore, molto probabilmente sovraccarico. E nel tuo codice sono sovraccarichi reali. In particolare, è necessario capire il tipo di (B|C) prima che compia la risoluzione di overload di = . Quindi è abbastanza chiaro che il tipo di A non può ragionevolmente influenzare B|C , perché A non è nemmeno considerato in quel momento.

L'argomento è più debole per

some_peripheral1.reg1  = some_peripheral<some_peripheral_base>::reg1_bits::AM;
some_peripheral1.reg1 |= some_peripheral<some_peripheral_base>::reg1_bits::BAR;

ma anche in questo caso i tipi da entrambi i lati devono essere determinati separatamente prima di eseguire la risoluzione del sovraccarico e, una volta eseguita la risoluzione del sovraccarico, è troppo tardi per influire sulla ricerca del nome.

    
risposta data 30.11.2015 - 17:30
fonte

Leggi altre domande sui tag