Цель Союзов в C и C ++

254

Я раньше использовал союзы с комфортом; Сегодня я был встревожен, когда я прочитал этот пост и узнал, что этот код

union ARGB
{
    uint32_t colour;

    struct componentsTag
    {
        uint8_t b;
        uint8_t g;
        uint8_t r;
        uint8_t a;
    } components;

} pixel;

pixel.colour = 0xff040201;  // ARGB::colour is the active member from now on

// somewhere down the line, without any edit to pixel

if(pixel.components.a)      // accessing the non-active member ARGB::components

фактически является неопределенным поведением, т. е. чтение из члена объединения, отличного от недавно записанного, приводит к неопределенному поведению. Если это не предполагаемое использование союзов, что это? Кто-нибудь может объяснить это подробно?

Обновить:

Я хотел бы уточнить несколько вещей в ретроспективе.

  • Ответ на вопрос не одинаков для C и C ++; моя неосведомленная младшая личность пометила его как C и C ++.
  • После изучения стандарта C ++ 11 я не мог окончательно сказать, что он призывает к доступу / проверке неактивного члена объединения не определено / не определено / определяется реализацией. Все, что я мог найти, было §9.5 / 1:

    Если объединение стандартной компоновки содержит несколько структур стандартной компоновки, которые имеют общую начальную последовательность, и если объект этого типа объединения стандартной компоновки содержит одну из структур стандартной компоновки, разрешается проверять общую начальную последовательность любой членов структуры стандартного макета. §9.2 / 19: Две структуры стандартного макета разделяют общую начальную последовательность, если соответствующие элементы имеют типы, совместимые с макетом, и ни один из элементов не является битовым полем, либо оба являются битовыми полями с одинаковой шириной для последовательности из одного или более начальных члены.

  • В то время как в C ( C99 TC3 - DR 283 и далее) это допустимо ( спасибо Паскалю Куоку за то, что поднял этот вопрос). Однако попытка сделать это может привести к неопределенному поведению , если прочитанное значение окажется недопустимым (так называемое «представление ловушки») для типа, через которое оно читается. В противном случае значение read определяется реализацией.
  • В C89 / 90 это объясняется неопределенным поведением (Приложение J), а в книге K & R говорится, что реализация определена. Цитата из K & R:

    Это цель объединения - единственная переменная, которая может на законных основаниях содержать любой из нескольких типов. [...], пока использование является последовательным: извлеченный тип должен быть последним сохраненным типом. Программист обязан следить за тем, какой тип в данный момент хранится в объединении; результаты зависят от реализации, если что-то хранится как один тип и извлекается как другой.

  • Выписка из ТС ++ PL Страуструпа (выделено мое)

    Использование союзов может иметь важное значение для совместимости данных, [...] иногда неправильно используемых для «преобразования типов ».

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

legends2k
источник
11
Проще говоря, компиляторам разрешено вставлять отступы между элементами в структуре. Таким образом, b, g, r,и aможет не быть смежным, и, следовательно, не соответствовать макету uint32_t. Это в дополнение к проблемам Endianess, на которые указывали другие.
Томас Мэтьюз
8
Именно поэтому вы не должны отмечать вопросы C и C ++. Ответы разные, но поскольку ответчики даже не говорят, за какой тег они отвечают (они даже знают?), Вы получаете мусор.
Паскаль Куок
5
@ downvoter Спасибо, что не объяснили, я понимаю, что вы хотите, чтобы я волшебным образом понял вашу хватку и не повторял ее в будущем: P
legends2k
1
Что касается первоначального намерения иметь профсоюз , имейте в виду, что стандарт C устанавливает даты союзов C на несколько лет. Беглый взгляд на Unix V7 показывает несколько преобразований типов через объединения.
ниндзя
3
scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1...действительно? Вы цитируете примечание об исключении , а не главное в самом начале абзаца : «В объединении не более одного элемента не статических данных может быть активным в любое время, то есть значение не более одного из Нестатические члены данных могут быть сохранены в объединении в любое время. " - и вплоть до p4: «В общем, нужно использовать явные вызовы деструктора и размещение новых операторов для изменения активного члена объединения »
underscore_d

Ответы:

409

Цель профсоюзов довольно очевидна, но по некоторым причинам люди часто упускают ее.

Цель объединения - сохранить память , используя одну и ту же область памяти для хранения разных объектов в разное время. Вот и все.

Это как комната в отеле. Разные люди живут в нем в течение непересекающихся периодов времени. Эти люди никогда не встречаются и вообще ничего не знают друг о друге. Правильно управляя распределением времени между комнатами (т.е. следя за тем, чтобы разные люди не были назначены на одну комнату одновременно), относительно небольшая гостиница может предоставить жилье относительно большому количеству людей, то есть, какие гостиницы для.

Это именно то, что делает союз. Если вы знаете, что несколько объектов в вашей программе содержат значения с неперекрывающимися значениями времени жизни, вы можете «объединить» эти объекты в объединение и тем самым сэкономить память. Точно так же, как в гостиничном номере в каждый момент времени имеется не более одного «активного» арендатора, в профсоюзе в каждый момент времени программы может быть не более одного «активного» члена. Только «активный» член может быть прочитан. Записав другого участника, вы переключаете «активный» статус на другого участника.

По какой-то причине эта первоначальная цель объединения была «переопределена» чем-то совершенно иным: написание одного члена союза, а затем проверка его через другого члена. Этот вид реинтерпретации памяти (так называемый "наказание типа") не является допустимым использованием союзов. Обычно это приводит к неопределенному поведению , описанному как создание определяемого реализацией поведения в C89 / 90.

РЕДАКТИРОВАТЬ: Использование союзов для целей типа наказания (т.е. написание одного члена, а затем чтение другого) было дано более подробное определение в одном из Технических исправлений к стандарту C99 (см. DR # 257 и DR # 283 ). Однако имейте в виду, что формально это не защищает вас от непреднамеренного поведения при попытке прочитать представление ловушки.

Муравей
источник
37
+1 за продуманность, приведение простого практического примера и рассказ о наследии союзов!
legends2k
6
Проблема с этим ответом состоит в том, что большинство операционных систем, которые я видел, имеют заголовочные файлы, которые делают именно это. Например, я видел это в старых (до 64-битных) версиях <time.h>как для Windows, так и для Unix. Отклонение его как «недопустимый» и «неопределенный» на самом деле не достаточно, если меня попросят понять код, который работает именно таким образом.
Тед
31
@AndreyT «Никогда не было законно использовать союзы для наказания типов до самого недавнего времени»: 2004 год не «очень недавний», особенно если учесть, что изначально только C99 был неуклюже сформулирован и, по-видимому, делает наказание типов через союзы неопределенными. В действительности, наказание за профсоюзы является законным в C89, законным в C11, и это было законным в C99 все время, хотя комитету потребовалось до 2004 года, чтобы исправить неправильную формулировку, и последующее издание TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Паскаль Куок
6
@ legends2k Язык программирования определяется стандартом. В Техническом исправлении 3 стандарта C99 прямо указывается на ввод текста в сноске 82, которую я предлагаю вам прочитать для себя. Это не телевидение, где рок-звезды дают интервью и высказывают свое мнение об изменении климата. Мнение Страуструпа не имеет никакого влияния на то, что говорится в стандарте C.
Паскаль Куок
6
@ legends2k " Я знаю, что мнение любого человека не имеет значения, и имеет значение только стандарт " Мнение авторов компиляторов имеет гораздо большее значение, чем (крайне плохая) языковая "спецификация".
любопытный парень
38

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

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Эрих Кицмюллер
источник
Я полностью согласен, не входя в хаос неопределенного поведения, возможно, это лучшее намерение союзов, о котором я могу думать; но это не пустая трата места, когда я просто использую, скажем, intили char*для 10 предметов объекта []; в этом случае я могу фактически объявить отдельные структуры для каждого типа данных вместо VAROBJECT? Разве это не уменьшит беспорядок и не займет меньше места?
legends2k
3
легенды: в некоторых случаях вы просто не можете этого сделать. Вы используете что-то вроде VAROBJECT в C в тех же случаях, когда вы используете Object в Java.
Эрих Кицмюллер
Как вы объясняете, структура данных помеченных союзов представляется единственно законным использованием союзов.
legends2k
Также приведите пример использования значений.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 六四 事件 法轮功 Может помочь часть примера из C ++ Primer . wandbox.org/permlink/cFSrXyG02vOSdBk2
Рик,
34

Поведение не определено с языковой точки зрения. Учтите, что разные платформы могут иметь разные ограничения в выравнивании памяти и порядке байтов. Код с прямым порядком байтов по сравнению с машиной с прямым порядком байтов будет по-разному обновлять значения в структуре. Исправление поведения в языке потребовало бы, чтобы все реализации использовали один и тот же порядок байтов (и ограничения выравнивания памяти ...), ограничивающие использование.

Если вы используете C ++ (вы используете два тега) и действительно заботитесь о переносимости, то вы можете просто использовать структуру и предоставить установщик, который принимает uint32_tи устанавливает поля соответствующим образом с помощью операций битовой маски. То же самое можно сделать в Си с помощью функции.

Редактировать : я ожидал, что AProgrammer запишет ответ для голосования и закроет его. Как отмечалось в некоторых комментариях, порядок байтов рассматривается в других частях стандарта, позволяя каждой реализации решать, что делать, а выравнивание и заполнение также могут обрабатываться по-разному. Теперь, строгие правила псевдонимов, на которые AProgrammer неявно ссылается, являются здесь важным моментом. Компилятору разрешается делать предположения о модификации (или отсутствии модификации) переменных. В случае объединения компилятор может переупорядочить инструкции и переместить чтение каждого компонента цвета поверх записи в переменную цвета.

Дэвид Родригес - дрибеи
источник
+1 за ясный и простой ответ! Я согласен, для переносимости, метод, который вы дали во втором параграфе, остается в силе; но могу ли я использовать способ, который я поставил в вопросе, если мой код привязан к одной архитектуре (платит цену переносимости), поскольку он экономит 4 байта для каждого значения пикселя и некоторое время, сэкономленное при выполнении этой функции ?
legends2k
Проблема с порядком байтов не заставляет стандарт объявлять его как неопределенное поведение - reinterpret_cast имеет точно такие же проблемы с порядком байтов, но имеет поведение, определяемое реализацией.
JoeG
1
@ legends2k, проблема в том, что оптимизатор может предположить, что uint32_t не изменен путем записи в uint8_t, и поэтому вы получаете неправильное значение, когда оптимизированное использование этого предположения ... @Joe, неопределенное поведение появляется, как только вы получаете доступ к указатель (я знаю, есть некоторые исключения).
AProgrammer
1
@ legends2k / AProgrammer: результат reinterpret_cast определяется реализацией. Использование возвращенного указателя не приводит к неопределенному поведению, только к поведению, определяемому реализацией. Другими словами, поведение должно быть согласованным и определенным, но оно не переносимо.
JoeG
1
@ legends2k: любой приличный оптимизатор распознает побитовые операции, которые выбирают целый байт, и генерирует код для чтения / записи байта, такой же как объединение, но четко определенный (и переносимый). например, uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | р; }
Бен Фойгт
22

Самое частое использование, с которым unionя регулярно сталкиваюсь - это псевдонимы .

Учтите следующее:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

Что это делает? Это позволяет чистый, аккуратный доступ к Vector3f vec;членам с любым именем:

vec.x=vec.y=vec.z=1.f ;

или с помощью целочисленного доступа в массив

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

В некоторых случаях доступ по имени - это самое ясное, что вы можете сделать. В других случаях, особенно когда ось выбирается программно, проще всего получить доступ к оси по числовому индексу - 0 для x, 1 для y и 2 для z.

bobobobo
источник
3
Это также называется, type-punningчто также упоминается в вопросе. Также пример в вопросе показывает аналогичный пример.
legends2k
4
Это не тип наказания. В моем примере типы совпадают , поэтому «каламбура» нет, это просто псевдонимы.
Бобобобо
3
Да, но, тем не менее, с абсолютной точки зрения языкового стандарта члены, написанные и прочитанные, отличаются, что не определено, как упомянуто в вопросе.
legends2k
3
Я хотел бы надеяться, что будущий стандарт исправит этот конкретный случай, который будет разрешен в соответствии с правилом «общая начальная подпоследовательность». Однако массивы не участвуют в этом правиле в соответствии с текущей формулировкой.
Бен Фойгт
3
@curiousguy: Очевидно, что не требуется размещать элементы структуры без произвольного заполнения. Если код проверяет размещение элемента структуры или размер структуры, код должен работать, если доступ осуществляется непосредственно через объединение, но строгое чтение Стандарта указывает на то, что получение адреса члена объединения или структуры приводит к указателю, который нельзя использовать как указатель своего собственного типа, но сначала должен быть преобразован обратно в указатель на включающий тип или символьный тип. Любой удаленно работающий компилятор расширит язык, заставляя работать больше вещей, чем ...
суперкат
10

Как вы говорите, это строго неопределенное поведение, хотя оно будет «работать» на многих платформах. Настоящая причина использования союзов - это создание вариантов записей.

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

Конечно, вам также нужен какой-то дискриминатор, чтобы сказать, что на самом деле содержит вариант. И обратите внимание, что в C ++ объединения не очень полезны, потому что они могут содержать только POD-типы - фактически те, которые не имеют конструкторов и деструкторов.


источник
Вы использовали это таким образом (как в вопросе)? :)
legends2k
Это немного педантично, но я не совсем принимаю "вариант записи". То есть я уверен, что они имели в виду, но если они были приоритетом, почему бы не предоставить их? «Предоставьте строительный блок, потому что это может быть полезно для создания других вещей», просто кажется более интуитивно более вероятным. Особенно учитывая, по крайней мере, еще одно приложение, которое, вероятно, было
задумано
@ Stev314 Если бы это было использование, которое они имели в виду, они могли бы сделать это не неопределенным поведением.
@Neil: +1 для первого, кто скажет о фактическом использовании, не затрагивая неопределенное поведение. Я предполагаю, что они могли бы сделать его реализацию определенной, как другие операции перетаскивания типа (reinterpret_cast и т. Д.). Но, как я и спросил, ты использовал это для наказания?
legends2k
@Neil - пример отображаемого в память регистра не является неопределенным, обычным порядком байтов / etc, и ему присвоен флаг «volatile». Запись по адресу в этой модели не ссылается на тот же регистр, что и чтение по тому же адресу. Поэтому нет проблемы «что вы читаете», потому что вы не читаете обратно - какой бы вывод вы ни записали по этому адресу, когда вы читаете, вы просто читаете независимый ввод. Единственная проблема - убедиться, что вы прочитали входную сторону объединения и напишите выходную сторону. Был распространен во встроенном материале - вероятно, до сих пор.
Steve314
8

В Си это был хороший способ реализовать что-то вроде варианта.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

Во времена маленькой памяти эта структура использует меньше памяти, чем структура, в которой есть все члены.

Кстати, С обеспечивает

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

для доступа к битовым значениям.

Totonga
источник
Хотя оба ваших примера прекрасно определены в стандарте; но, эй, использование битовых полей - это, конечно, непереносимый код, не так ли?
legends2k
Нет, это не так. Насколько я знаю, его широко поддерживают.
Тотонга
1
Поддержка компилятора не переводится на переносимый. Книга C : C (и, следовательно, C ++) не дает гарантии упорядочения полей внутри машинных слов, поэтому, если вы используете их по последней причине, ваша программа будет не только непереносимой, но и зависимой от компилятора.
legends2k
5

Хотя это строго неопределенное поведение, на практике оно будет работать практически с любым компилятором. Это настолько широко используемая парадигма, что любой уважающий себя компилятор должен будет делать «правильные вещи» в таких случаях, как этот. Это, безусловно, предпочтительнее, чем наказание типов, которое может генерировать испорченный код с некоторыми компиляторами.

Пол Р
источник
2
Разве нет проблемы с порядком байтов? Относительно легкое исправление по сравнению с «неопределенным», но в некоторых случаях стоит принять во внимание некоторые проекты.
Steve314
5

В C ++, Boost Variant реализует безопасную версию объединения, разработанную для максимально возможного предотвращения неопределенного поведения.

Его характеристики идентичны enum + unionконструкции (стек выделен и т. Д.), Но он использует список шаблонов типов вместо enum:)

Матье М.
источник
5

Поведение может быть неопределенным, но это просто означает, что не существует «стандарта». Все достойные компиляторы предлагают #pragmas для управления упаковкой и выравниванием, но могут иметь разные значения по умолчанию. Значения по умолчанию также будут меняться в зависимости от используемых настроек оптимизации.

Кроме того, профсоюзы не только для экономии места. Они могут помочь современным компиляторам с типом штамповки. Если вы reinterpret_cast<>все, компилятор не может делать предположения о том, что вы делаете. Возможно, придется выбросить то, что он знает о вашем типе, и начать заново (принудительная запись в память, что в наши дни очень неэффективно по сравнению с тактовой частотой процессора).

Ник
источник
4

Технически это не определено, но на самом деле большинство (все?) Компиляторов обрабатывают его точно так же, как и использование reinterpret_castодного типа к другому, результатом которого является реализация реализации. Я не потерял бы сон из-за вашего текущего кода.

JoeG
источник
" reinterpret_cast от одного типа к другому, результат которого определяется реализацией. " Нет, это не так. Реализации не должны определять это, и большинство не определяет это. Кроме того, что будет разрешено реализацией, определяющей поведение приведения некоторого случайного значения к указателю?
любопытный парень
4

Для еще одного примера фактического использования объединений платформа CORBA сериализует объекты, используя подход с теговым объединением. Все пользовательские классы являются членами одного (огромного) объединения, а целочисленный идентификатор сообщает демаршаллеру, как следует интерпретировать объединение.

Cubbi
источник
4

Другие упоминали различия в архитектуре (little - big endian).

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

например. union {float f; int i; } Икс;

Запись в xi была бы бессмысленной, если бы вы потом читали из xf - если только это не то, что вы намеревались для того, чтобы взглянуть на компоненты знака, экспоненты или мантиссы поплавка.

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

например. union {char c [4]; int i; } Икс;

Если гипотетически на некоторой машине символ должен быть выровнен по словам, то c [0] и c [1] будут совместно использовать память с i, но не c [2] и c [3].

philcolbourn
источник
Байт, который должен быть выровнен по слову? Это не имеет смысла. Байты не имеют требования выравнивания, по определению.
любопытный парень
Да, наверное, мне следовало использовать лучший пример. Спасибо.
Филколборн
@curiousguy: во многих случаях желательно, чтобы массивы байтов были выровнены по словам. Если один имеет много массивов, например, 1024 байта, и часто захочет копировать один в другой, то выравнивание по словам может во многих системах удвоить скорость передачи a memcpy()от одного к другому. Некоторые системы могут спекулятивно выравнивать char[]распределения, которые происходят вне структур / объединений по этой и другим причинам. В существующем примере предположение о том, что iвсе элементы будут перекрываться, c[]непереносимо, но это потому, что на это нет гарантии sizeof(int)==4.
суперкат
4

На языке Си, как это было задокументировано в 1974 году, все члены структуры имели общее пространство имен, и значение «ptr-> member» было определено как добавление смещения члена к «ptr» и доступ к результирующему адресу с использованием типа члена. Этот дизайн позволил использовать один и тот же ptr с именами элементов, взятыми из разных определений структуры, но с одинаковым смещением; Программисты использовали эту способность для различных целей.

Когда членам структуры были назначены их собственные пространства имен, стало невозможно объявить два члена структуры с одинаковым смещением. Добавление объединений к языку позволило достичь той же семантики, которая была доступна в более ранних версиях языка (хотя невозможность экспортировать имена в окружающий контекст, возможно, все еще требовала использования поиска / замены для замены элемента foo-> в foo-> type1.member). Важно было не столько, чтобы люди, которые добавляли союзы, имели в виду какую-то конкретную цель использования, а скорее то, что они предоставляют средство, с помощью которого программисты, которые полагались на более раннюю семантику, для какой-либо цели , все еще могли бы достичь та же семантика, даже если они должны были использовать другой синтаксис для этого.

Supercat
источник
Цените урок истории, однако со стандартом, определяющим такие и такие, как неопределенное, чего не было в ушедшей эпохе C, когда книга K & R была единственным «стандартом», нужно быть уверенным в том, что она не используется ни для каких целей и войти в землю UB.
legends2k
2
@ legends2k: Когда был написан стандарт, большинство реализаций C одинаково относились к объединениям, и такая обработка была полезной. Некоторые, однако, этого не сделали, и авторы Стандарта не хотели маркировать любые существующие реализации как «не соответствующие». Вместо этого они полагали, что, если разработчикам не нужен Стандарт, чтобы сказать им что-то делать (о чем свидетельствует тот факт, что они уже это делают ), оставив его неуказанным или неопределенным, просто сохранится статус-кво . Идея, что это должно сделать вещи менее определенными, чем они были до написания Стандарта ...
Суперкат 22.09.16
2
... кажется гораздо более свежим нововведением. Что особенно печально во всем этом, так это то, что если создатели компиляторов, нацеленные на высокопроизводительные приложения, должны были выяснить, как добавить полезные директивы оптимизации к языку, который большинство компиляторов реализовало в 1990-х годах, а не потрошить функции и гарантии, которые поддерживались «только» «В 90% реализаций результатом будет язык, который мог бы работать лучше и надежнее, чем
гиперсовременный
2

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

  1. Удобный способ получить доступ к одним и тем же данным разными способами, как в вашем примере
  2. Способ экономии места при наличии разных элементов данных, из которых только один может быть «активным»

На самом деле это скорее хакерский стиль в стиле C для быстрого написания кода на основе того, что вы знаете, как работает архитектура памяти целевой системы. Как уже говорилось, вы можете сойти с рук, если на самом деле не ориентированы на множество разных платформ. Я полагаю, что некоторые компиляторы могут позволять вам также использовать директивы упаковки (я знаю, что они используют для структур)?

Хороший пример 2. можно найти в типе VARIANT, широко используемом в COM.

Мистер Бой
источник
2

Как уже упоминалось, объединения, объединенные с перечислениями и заключенные в структуры, могут использоваться для реализации теговых объединений. Одним из практических применений является реализация Rust Result<T, E>, которая изначально реализована с использованием чистого enum(Rust может содержать дополнительные данные в вариантах перечисления). Вот пример C ++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
Kotauskas
источник