Определяемый пользователем класс перечисления С++ 11 Конструктор по умолчанию

Есть ли способ указать конструктор по умолчанию для enum class?

Я использую enum class, чтобы указать набор значений, допустимых для определенного типа данных в библиотеке: в данном случае это номера идентификаторов выводов GPIO Raspberry Pi. Это выглядит примерно так:

enum class PinID : int {N4 = 4, N17 = 17, /* ...etc... */ }

Смысл в том, что я делаю это вместо того, чтобы просто использовать, скажем, int, чтобы убедиться, что код безопасен: я могу static_assert (или иначе во время компиляции гарантировать - фактический используемый метод не важен для меня) такие вещи, как те, которые кто-то не не сделал орфографическую ошибку (пропустил 5 вместо 4 и т. д.), и я получаю автоматические сообщения об ошибках при несоответствии типов и т. д.

Тогда проблема заключается в том, что enum class имеет конструктор по умолчанию, который - я полагаю, ради совместимости с enums C (поскольку у них одинаковое поведение) - инициализируется enum class эквивалентом 0. В этом случае значение 0 отсутствует. Это означает, что пользователь делает объявление/определение, например:

PinID pid = PinID();

получает перечислитель, который явно не определен (и даже не кажется «существующим», если взглянуть на код), и может привести к ошибкам во время выполнения. Это также означает, что такие методы, как switching над значениями явно определенных перечислителей, невозможны без наличия случая ошибки/по умолчанию - чего я хочу избежать, поскольку это заставляет меня либо throw, либо делать что-то вроде возврата boost::optional, что менее приемлемо. к статическому анализу.

Я пытался определить конструктор по умолчанию безрезультатно. Я (отчаянно) пытался определить функцию, которая имеет то же имя, что и enum class, но это (довольно неудивительно) привело к странным ошибкам компилятора. Я хочу сохранить возможность приведения enum class к int, при этом все перечислители N# сопоставляются с их соответствующими #, поэтому простое "определение", скажем, N4 = 0 неприемлемо; это для простоты и здравомыслия.

Я предполагаю, что мой вопрос двоякий: есть ли способ получить статическую безопасность, которую я получаю после использования enum class? Если нет, то какие другие возможности можно было бы предпочесть? Я хочу что-то, что:

  1. является конструктивным по умолчанию
  2. можно сделать конструкцию по умолчанию произвольной допустимой величиной
  3. предоставляет «конечный набор указанных» значений, предоставляемых enum classes
  4. по крайней мере так же безопасен, как enum class
  5. (предпочтительно) не включает полиморфизм во время выполнения

Причина, по которой мне нужна конструктивность по умолчанию, заключается в том, что я планирую использовать boost::lexical_cast для уменьшения синтаксических издержек, связанных с преобразованиями между значениями enum class и фактическими связанными string, которые я вывожу в операционную систему (в данном случае sysfs); boost::lexical_cast требует конструктивности по умолчанию.

Ошибки в моих рассуждениях приветствуются — я начинаю подозревать, что в данном случае enum classes — правильный объект для неправильной работы; разъяснение будет предложено, если спросят. Спасибо за ваше время.


person FizzixNerd    schedule 13.07.2013    source источник


Ответы (3)


Тип, определенный с помощью enum class или enum struct, является не классом, а перечислением с ограниченной областью действия, и для него не может быть определен конструктор по умолчанию. Стандарт C++11 определяет, что ваш оператор PinID pid = PinID(); даст нулевую инициализацию. Где PinID был определен как enum class. Это также позволяет типам перечисления вообще содержать значения, отличные от констант перечислителя.

Чтобы понять, что PinID() дает нулевую инициализацию, необходимо вместе прочитать стандартные разделы 3.9.9, 8.5.5, 8.5.7 и 8.5.10:

8.5.10 - An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized

8.5.7 - To value-initialize an object of type T means: ... otherwise, the object is zero-initialized.

8.5.5 - To zero-initialize an object or reference of type T means: — if T is a scalar type (3.9), the object is set to the value 0 (zero), taken as an integral constant expression, converted to T;

3.9.9. Указывает, что перечисляемые типы являются частью набора типов, известных как скалярные типы.

Возможное решение:

Чтобы выполнить ваши пункты с 1 по 5, вы можете написать класс в следующем порядке:

class PinID
{
private:
    PinID(int val)
    : m_value(val)
    {}

    int m_value;

public:
    static const PinID N4;
    static const PinID N17;
    /* ...etc... */ 

    PinID() 
    : m_value(N4.getValue())
    {}

    PinID(const PinID &id)
    : m_value(id.getValue())
    {}

    PinID &operator = (const PinID &rhs)
    {
        m_value = rhs.getValue();
        return *this;
    }

    int getValue() const
    {
        return m_value;
    }

    // Attempts to create from int and throw on failure.
    static PinID createFromInt(int i);

    friend std::istream& operator>>(std::istream &is, PinID &v)
    {
        int candidateVal(0);
        is >> candidateVal;
        v = PinID::createFromInt(candidateVal);
        return is;
    }
};

const PinID PinID::N4 = PinID(4);
/* ...etc... */

Это может дать вам то, что вам придется приложить определенные усилия, чтобы получить недопустимые значения. Конструктор по умолчанию и оператор потока должны позволять ему работать с lexical_cast.

Кажется, это зависит от того, насколько критичны операции с PinID после его создания, стоит ли писать класс или просто обрабатывать недопустимые значения везде, где используется значение.

person PeterSW    schedule 13.07.2013
comment
Просто глядя на это, это выглядит в основном хорошо, но будет ли это работать? У вас есть определение класса, которое включает экземпляр самого себя в качестве члена... о, но они статичны. Тогда компилятор не злится. Умный медведь. - person FizzixNerd; 15.07.2013
comment
@FizzixNerd Я не запускал его через компилятор, но он должен работать ... Я думаю, что если вы добавите операторы потока, вы сможете использовать его напрямую с lexical_cast, я полагаю, что будет выдано исключение, если будет получено недопустимое значение. - person PeterSW; 15.07.2013
comment
@PeterSW, не могли бы вы указать, на какие элементы стандарта С++ 11 вы ссылаетесь, говоря об нулевой инициализации? - person Alexander Kirov; 24.04.2014
comment
@PeterSW, спасибо! Но также необходимо одно примечание: 5.2.3(2): выражение T(), где T — это спецификатор простого типа или спецификатор имени типа для полного типа объекта, не являющегося массивом, или (возможно, cv-квалифицированный) тип void, создает значение prvalue указанного типа, которое инициализируется значением (8.5; для случая void() инициализация не выполняется). [Примечание: если T является типом, не относящимся к классу, который является cv-квалифицированным, cv-квалификаторы игнорируются при определении типа результирующего значения prvalue (3.10). —конец примечания ] ------ было сложно поймать... - person Alexander Kirov; 25.04.2014

enum class — это просто строго типизированный enum; это не class. C++11 просто повторно использовал существующее ключевое слово class, чтобы избежать введения нового ключевого слова, которое нарушило бы совместимость с устаревшим кодом C++.

Что касается вашего вопроса, нет способа гарантировать во время компиляции, что приведение включает подходящего кандидата. Рассмотреть возможность:

int x;
std::cin >> x;
auto p = static_cast<PinID>(x);

Это совершенно законно, и нет никакого способа статически убедиться, что пользователь консоли поступил правильно.

Вместо этого вам нужно будет проверить во время выполнения, что значение допустимо. Чтобы обойти это автоматизированным способом, один из моих коллег создал генератор enum, который создает эти проверки, а также другие полезные подпрограммы, учитывая файл со значениями перечисления. Вам нужно будет найти решение, которое работает для вас.

person chrisaycock    schedule 13.07.2013
comment
Спасибо за ответ! Я знаю, что класс перечисления не является классом, но у него все еще есть конструктор по умолчанию. Что касается статической проверки, то я, конечно, знаю, что не могу статически проверить преобразования из string-›enum — для этого я использую boost::optioanls, но я могу статически проверить по крайней мере некоторые строки перечисления, содержащие constexpr, не так ли? - person FizzixNerd; 13.07.2013
comment
У @FizzixNerd есть конструктор по умолчанию. Технически нет. Он имеет определенное поведение для инициализации значения. Инициализация значения объекта типа класса вызывает конструктор по умолчанию; инициализация значения объекта типа numeric или enum устанавливает его в ноль; инициализация значения объекта типа указателя устанавливает его в значение нулевого указателя. - person aschepler; 18.07.2013

Я знаю, что этот вопрос устарел и что на него уже есть принятый ответ, но вот метод, который может помочь в такой ситуации с некоторыми из новых функций С++.

Вы можете объявить переменную этого класса либо non static, либо static, это можно сделать несколькими способами, разрешенными при поддержке вашего текущего компилятора.


Нестатические:

#include <iostream>
#include <array>

template<unsigned... IDs>
class PinIDs {
private:
    const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:
    PinIDs() = default;
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

Статический: - Есть 3 способа написать это: (Первый - C++11 или 14 или выше) последние 2 (C++17).

Не цитируйте меня в части C++11; Я не совсем уверен, когда впервые появились вариативные шаблоны или пакеты параметров.

template<unsigned... IDs>
class PinIDs{
private:        
    static const std::array<unsigned, sizeof...(IDs)> ids;
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
const std::array<unsigned, sizeof...(IDs)> PinIDs<IDs...>::ids { IDs... };

template<unsigned... IDs>
class PinIDs{
private:
    static constexpr std::array<unsigned, sizeof...(IDs)> ids { IDs... }; 
public:   
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
class PinIDs{
private:
    static inline const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

Все приведенные выше примеры, как нестатические, так и статические, работают с одним и тем же вариантом использования ниже и дают правильные результаты:

int main() {
    PinIDs<4, 17, 19> myId;

    std::cout << myId[0] << " ";
    std::cout << myId[1] << " ";
    std::cout << myId[2] << " ";

    std::cout << "\nPress any key and enter to quit." << std::endl;
    char c;
    std::cin >> c;

    return 0;
}

Вывод

4 17 19
Press any key and enter to quit.

С этим типом шаблона класса, использующего вариативный список параметров, вам не нужно использовать какой-либо конструктор, кроме конструктора по умолчанию. Я добавил в массив проверку границ, чтобы operator[] не превышала границ своего размера; Я мог бы выдать ошибку, но с типом unsigned я просто вернул -1 как недопустимое значение.

С этим типом нет значения по умолчанию, так как вам нужно создать экземпляр объекта такого типа через список параметров шаблона с одним или набором значений. Если кто-то хочет, они могут specialize this class с одним параметром 0 для типа по умолчанию. Когда вы создаете экземпляр этого типа объекта; он окончательный, так как его нельзя изменить из его объявления. Это константный объект, который по-прежнему считается конструируемым по умолчанию.

person Francis Cugler    schedule 20.12.2017