constexpr и инициализация

#c #initialization #constants #c 11

#c #инициализация #константы #c 11

Вопрос:

Возможно, что-то подобное уже задавалось, и, конечно, это придирка…

У меня есть куча констант std::map для переключения между enum (class) значениями и их std::string представлениями (в обоих направлениях). Кто-то здесь указал мне, что эти карты будут инициализированы во время выполнения, когда будет запущен другой код инициализации, прежде чем моя программа выполнит все необходимые действия. Это означало бы, что постоянные выражения повлияют на производительность во время выполнения, поскольку карты создаются из их пар перечисляемых строк.

В качестве наглядного примера приведем пример одной из таких карт:

 enum class os
{
    Windows,
    Linux,
    MacOSX
};
const map<string, os> os_map =
     { {"windows", os::Windows},
       {"linux",   os::Linux},
       {"mac",     os::MacOSX} };
const map<os, string> os_map_inverse =
     { {os::Windows, "windows"},
       {os::Linux,   "linux"},
       {os::MacOSX,  "mac"} };
  

Будет ли C 11 constexpr иметь какое-либо влияние на производительность, или мое предположение о штрафе за инициализацию во время выполнения неверно? Я бы подумал, что компилятор может встроить постоянный контейнер STL в виде чистых данных в исполняемый файл, но, видимо, это может быть не так просто, как я пытаюсь представить?

Комментарии:

1. Почему бы вам не попробовать boost::bimap двустороннее сопоставление между перечислением и его строковым представлением? Гораздо меньше вероятность ошибки при добавлении новых значений.

2. Xeo: включить Boost для такой простой вещи, как эта? Нет, спасибо, я свободен от зависимостей и действительно хотел бы сохранить это таким образом ;)… Я мог бы даже заменить отображение string-> enum на unordered_map и отображение enum-> string на vector (значения enum не важны, они просто подсчитываются одно за другим) для повышения производительности, если это что-то улучшит. boost::bimap было бы отстой в сравнении 🙂

3. @rubenvb : И все же повысить. MultiIndex мог бы сделать именно это, гораздо более кратко, с 0 накладными расходами. Пожалуйста, не рассматривайте Boost как «зависимость».

Ответ №1:

Проблема заключается не столько в производительности инициализации, сколько в порядке инициализации. Если кто-то использует вашу карту до main запуска (например, при инициализации переменной области видимости пространства имен), то вы SOL, потому что вам не гарантируется, что ваша карта была инициализирована до того, как ее использует инициализация пользователя.

Однако вы можете сделать это во время компиляции. Строковые литералы являются постоянными выражениями, как и перечислители. Простая структура сложности с линейным временем

 struct entry {
  char const *name;
  os value;
};

constexpr entry map[] = {
  { "windows", os::Windows },
  { "linux", os::Linux },
  { "mac", os::Mac }
};

constexpr bool same(char const *x, char const *y) {
  return !*x amp;amp; !*y ? true : (*x == *y amp;amp; same(x 1, y 1));
}

constexpr os value(char const *name, entry const *entries) {
  return same(entries->name, name) ? entries->value : value(name, entries 1);
}
  

Если вы используете value(a, b) в контексте постоянного выражения, а указанное вами имя не существует, вы получите ошибку времени компиляции, поскольку вызов функции станет непостоянным.

Для использования value(a, b) в контексте непостоянного выражения вам лучше добавить функции безопасности, такие как добавление конечного маркера в ваш массив и создание исключения value , если вы нажмете на конечный маркер (вызов функции все равно будет постоянным выражением, пока вы никогда не нажмете на конечный маркер).

Комментарии:

1. Кажется, это не работает (GCC 4.5.1): ideone.com/w8QFN . Вы думаете, это проблема компилятора?

2. @atzz да, это проблема компилятора. Попробуйте GCC4.6.

3. Йоханнес, спасибо за ответ; я сделаю это завтра. На данный момент у вас нет доступного компилятора.

4. С GCC4.6 это работает так, как ожидалось, и действительно gcc.gnu.org/projects/cxx0x.html показывает, что constexpr поддержка была добавлена в версии 4.6. Йоханнес, извините за все это взад-вперед; Я не должен был заниматься C в воскресенье вечером 😉

5. !*x amp;amp; !*y возвращает значение true для обоих nullptr. Верно? Так почему true ? Что должно возвращаться nullptr==nullptr ? Что касается меня false . И также должна быть if( x==y ) простая оптимизация.

Ответ №2:

constexpr не работает с произвольными выражениями, и особенно не с вещами, которые будут использовать свободное хранилище. map / string будет использовать свободное хранилище, и, следовательно, constexpr не будет работать для их инициализации во время компиляции и не будет запускать код во время выполнения.

Что касается штрафа во время выполнения: в зависимости от продолжительности хранения этих переменных (и я предполагаю, что здесь static, что означает инициализацию перед main), вы даже не сможете измерить штраф, особенно в коде, который их использует, предполагая, что вы будете использовать их много раз, где поиск имеет гораздо больше «накладных расходов», чем инициализация.

Но что касается всего, помните правило первое: заставляйте вещи работать. Профиль. Делайте все быстро. В таком порядке.

Ответ №3:

Ах да, это типичная проблема.

Единственная альтернатива, которую я нашел, чтобы избежать этой инициализации во время выполнения, — использовать простые структуры C. Если вы готовы приложить дополнительные усилия и сохранить значения в простом массиве C, вы можете получить статическую инициализацию (и уменьшить объем памяти).

Это одна из причин, по которой LLVM / Clang фактически используют утилиту tblgen: простые таблицы статически инициализируются, и вы можете генерировать их отсортированными.

Другим решением было бы создать вместо этого специальную функцию: для преобразования перечисления в строку легко использовать переключатель и позволить компилятору оптимизировать его в простую таблицу, для строки для перечисления это немного сложнее (вам нужны ветви if / else, организованные правильно, чтобы получить поведение O (log N)), но для небольших перечислений линейный поиск в любом случае хорош, и в этом случае один макрос-хакерство (основанное на совершенстве препроцессора Boost) может дать вам все, что вам нужно.

Ответ №4:

Недавно я нашел свой собственный лучший способ представить перечисление. Смотрите код:

 enum State {
    INIT, OK, ENoFou, EWroTy, EWroVa
};

struct StateToString {
    State state;
    const char *name;
    const char *description;
public:
    constexpr StateToString(State constamp; st)
            : state(st)
            , name("Unknown")
            , description("Unknown")
    {
        switch(st){
            case INIT  : name = "INIT"  , description="INIT";              break;
            case OK    : name = "OK"    , description="OK";                break;
            case ENoFou: name = "ENoFou", description="Error: Not found";  break;
            case EWroTy: name = "EWroTy", description="Error: Wrong type"; break;
            case EWroVa: name = "EWroVa", description="Error: Wrong value, setter rejected"; break;
            // Do not use default to receive a comile-time warning about not all values of enumeration are handled. Need `-Wswitch-enum` in GCC
        }
    }
};
  

Расширяя этот пример до некоторого универсального кода, давайте назовем его lib code:

 /// Concept of Compile time meta information about (enum) value.
/// This is the best implementation because:
///   - compile-time resolvable using `constexpr name = CtMetaInfo<T>(value)`
///   - enum type can be implemented anywhere
///   - compile-time check for errors, no hidden run-time surprises allowed - guarantied by design. 
///   - with GCC's `-Wswitch` or `-Werror=switch` - you will never forget to handle every enumerated value
///   - nice and simple syntaxis `CtMetaInfo(enumValue).name`
///   - designed for enums suitable for everything
///   - no dependencies
/// Recommendations:
///   - write `using TypeToString = CtMetaInfo<Type>;` to simplify code reading
///   - always check constexpr functions assigning their return results to constexpr values
/**code

    template< typename T >
    concept CtMetaInfo_CONCEPT {
        T value;
        const char *name;
        // Here could add variables for any other meta information. All vars must be filled either by default values or in ctor init list
    public:
        ///param defaultName will be stored to `name` if constructor body will not reassign it
        constexpr CtMetaInfoBase( T constamp; val, const char* defaultName="Unknown" );
    };

   endcode
 */

/// Pre-declare struct template. Specializations must be defined.
template< typename T >
struct CtMetaInfo;

/// Template specialization gives flexibility, i.e. to define such function templates
template <typename T>
constexpr const char* GetNameOfValue(T constamp; ty)
{ return CtMetaInfo<T>(ty).name; }
  

Пользователь должен определить специализацию для пользовательского типа:

 /// Specialization for `rapidjson::Type` 
template<>
struct CtMetaInfo<Type> {
    using T = Type;
    T value;
    const char *name;
public:
    constexpr CtMetaInfo(T constamp; val)
            : value(val)
            , name("Unknown")
    {
        switch(val){
            case kNullType                 : name = "Null"  ; break;
            case kFalseType: case kTrueType: name = "Bool"  ; break;
            case kObjectType               : name = "Object"; break;
            case kArrayType                : name = "Array" ; break;
            case kStringType               : name = "String"; break;
            case kNumberType               : name = "Number"; break;
        }
    }
    static constexpr const char* Name(T constamp; val){
        return CtMetaInfo<Type>(val).name;
    }
};

/// TEST
constexpr const char *test_constexprvalue = CtMetaInfo<Type>(kStringType).name;
constexpr const char *test_constexprvalu2 = GetNameOfValue(kNullType);