Страницы

Поиск по вопросам

Показаны сообщения с ярлыком language-lawyer. Показать все сообщения
Показаны сообщения с ярлыком language-lawyer. Показать все сообщения

четверг, 5 марта 2020 г.

В каком стандарте Си разрешили обьявлять переменные в любом месте функции?

#c #language_lawyer


В каком стандарте Си разрешили объявлять переменные в любом месте функции?
    


Ответы

Ответ 1



В любом месте - начиная с С99. Не совсем понятна, правда, привязка к функции. В классическом ANSI C (C89/90) тоже можно было делать локальные объявления "в любом месте функции", если это "место" являлось началом блока. Такая возможность появилась еще в достандартные времена - она присутствует уже в первом издании K&R C. Начиная же с С99 разрешается делать локальные объявления в любом месте блока. Требование выноса локальных объявлений в начало именно функции существовало только уж в совсем архаичных версиях языка, типа описанной в C Reference Manual.

вторник, 25 февраля 2020 г.

Перегрузка конструкторов, какой конструктор будет вызван?

#cpp #language_lawyer


Это UB или есть какое-то правило?

class foo {
public:
    foo (int a, int b) { ... }
    foo (char a, char b) { ... }
};

int main ()
{
    foo bar(1,'2');
}

    


Ответы

Ответ 1



Здесь нет неопределенного поведения, так как здесь вообще нет никакого поведения, так как код не будет компилироваться.:) Здесь имеет место неоднозначность вызова конструктора, так как ни один из конструкторов не является лучше другого для заданных аргументов. Согласно стандарту C++ (13.3.3 Best viable function) ...Given these definitions, a viable function F1 is defined to be a better > function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then... В вашем примере foo (int a, int b) { ... } foo (char a, char b) { ... } //... foo bar(1,'2'); первый параметр первого конструктора "лучше" первого параметра второго конструктора так как не требуется преобразования из типа аргумента к типу параметра первого конструктора. Однако относительно вторых параметров ситуация прямо противоположная, то есть второй параметр второго конструктора "лучше" второго параметра первого конструктора, так как не требуется никакого преобразования из типа аргумента к типу параметра. Следовательно согласно приведенной цитате ни один из конструкторов не лучше другого конструктора для всех своих параметров и заданных аргументов. Это UB или есть какое-то правило? Это называется разрешением перегрузки, и правила выбора компилятором наилучшей функции из перегруженных описаны в стандарте C++. 3 If a best viable function exists and is unique, overload resolution succeeds and produces it as the result. Otherwise overload resolution fails and the invocation is ill-formed.... Дополнение. При определенных опциях компилятор GCC, например, может скомпилировать код, но при этом выдать предупреждение. В этом случае он выберет первый конструктор, так как для его второго параметра достаточно применить к аргументу integral promotion. В то время как для второго конструктора для его первого параметра требуется преобразование аргумента из типа int в тип char А integral promotion имеет "лучший" ранг, чем преобразование (13.3.3.2 Ranking implicit conversion sequences): 4 Standard conversion sequences are ordered by their ranks: an Exact Match is a better conversion than a Promotion, which is a better conversion than a Conversion. Неоднозначность можно было бы (и следовало) устранить посредством явного преобразования, либо написав foo bar( 1, int( '2' ) ); и тогда будет вызван первый конструктор, либо написав foo bar( char( 1 ), '2' ); и тогда будет вызван второй конструктор.

Ответ 2



Да, это UB. Если ее скомпилировать g++, то будет использован конструктор foo (int a, int b) и вылезет warning: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second Clang отказался компилировать, выдав ошибку, майкрософтовский тоже, то есть то, какой конструктор будет выбран, зависит от реализации компилятора (g++ выбирает по принципу какое из плохих преобразований лучше, хотя как он решает, я не очень понял).

пятница, 14 февраля 2020 г.

Порядок вызова методов со схожей сигнатурой

#c_sharp #language_lawyer


Почему при выполнении этой программы this(x,y) будет ссылаться на A(float x, byte
y)? Как в общем случае производится выполнение программы, при схожих методах и с (не)явным
приведением? 

Что, если будет еще один public A(byte x, int y)?

class A
{
    public A(float x, short y) { Console.Write(1); }
    public A(float x, byte y) { Console.Write(2); }
    public A(double x, int y) { Console.Write(3); }
    public A(byte x, byte y, byte z):this(x,y) { Console.Write(4); }
}

static void Main()
{
    byte x = 1, y = 1, z = 1;
    A f = new A(x, y, z);
}

    


Ответы

Ответ 1



Смотрите, то что вас интересует — это порядок разрешения перегруженных имён (overload resolution). Окончательную информацию по этому поводу лучше всего смотреть в стандарте языка. В частности, за overload resolution отвечает раздел 7.5.3. Я перескажу релевантные части из стандарта. Для начала, создаётся список подходящих кандидатов. Затем, из этого списка выбирается один наилучшим образом подходящий кандидат. Если среди кандидатов нету такого кандидата, который подходит больше всех других, вызов функции-члена считается неоднозначным, и возникает ошибка привязки имён. Если наилучший подходящий кандидат не может быть использован (например потому, что он нестатический, а вызов происходит из статического контекста), возникает ошибка привязки имён. Подходящим кандидатом по отношению к данном списку аргументов A называется такая функция-член, что каждый аргумент из A соответствует параметру в декларации функции-члена, как описано в разделе Соответствующие параметры, а каждый параметр без соответствия можно поставить в соответствие необязательному параметру. Режим передачи аргумента (out, ref) соответствует режиму соответствующего параметра, а выражение-аргумент неявно приводимо к типу параметра, либо (для out/ref) совпадает с ним. Для аргумента в списке аргументов, соответствующим параметром является параметр с тем же индексом для позиционных, и с тем же именем для именованных аргументов. Итак, для нашего случая вызова this(x, y) все три конструктора с двумя параметрами являются подходящими: byte неявно приводим к всем типам double, float, short, byte. Далее, выбор наилучшего подходящего кандидата. Кандидат с типами параметров {P1, P2, ..., Pn} считается лучшим кандидатом, чем кандидат с типами параметров {Q1, Q2, ..., Qn} для нашего списка аргументов {E1, E2, ..., En}, если для каждого аргумента неявное преобразование Ex к Qx не лучше преобразования Ex к Px, а для хотя бы одного аргумента строго хуже. (Если все преобразования эквивалентны, то в игру вступают дополнительные правила, которые, например, предпочитают необобщённую функцию обобщённой.) Сравниваются преобразования так. Если тип Px совпадает с типом выражения (плюс хитрости для dynamic, плюс специальное рассмотрение для лямбд), а тип Qx не совпадает, то первое преобразование однозначно лучше. В противном случае, первое преобразование лучше, если либо нет неявного преобразования Qx к Px, а неявное преобразование Px к Qx есть, libo Px — знаковый числовой тип (или, возможно, Nullable), a Qx — беззнаковый (или его Nullable-собрат). В частности, sbyte — лучший целевой тип, чем byte, ushort, uint и ulong; short — лучший, чем ushort, uint и ulong; int — лучший, чем uint и ulong; long -- лучший, чем ulong. [Специальное рассмотрение для лямбд и Task пропущено.] Посмотрим, что это даёт для нас. У нас есть типы аргументов [byte, byte], и наборы типов параметров: [float, short], [float, byte], [double, int]. Второй список лучше, чем первый, т. к. преобразования из float в byte одинаковы, а преобразование из byte в byte лучше, чем из short. Второй список также лучше, чем третий, т. к. преобразования из double в byte хуже, чем из float в byte (т. к. float неявно конвертируется в double), и преобразование из int в byte хуже, чем из byte в byte. Таким образом, у нас второй список однозначно лучше всех. Если переставить параметры: public A(float x, short y) { Console.Write(1); } public A(double x, byte y) { Console.Write(2); } — код не скомпилируется. Как мы помним, преобразование из float лучше, чем из double, а из short хуже, чем из byte. Поэтому ни один из списков не лучше другого. Однако, можно заставить этот код скомпилироваться, используя именованные параметры: public A(float a, short b) { Console.Write(1); } public A(double x, byte y) { Console.Write(2); } public A(byte x, byte y, byte z) : this(a: x, b: y) { ... } Здесь в списке подходящих кандидатов оказывается только первый конструктор. Если вам в реальном коде понадобилось выяснять такие вот тонкости — скорее всего, вы злоупотребляете перегрузкой функций. В нормальных случаях правила работают очевидно правильным образом, а вот в сложных случаях может оказаться что-то неочевидным. Если вам такое встретилось, попробуйте дать функциям разные имена, а для конструкторов используйте идиому именованного конструктора (публичная статическая функция, возвращающая сконструированный объект).

понедельник, 10 февраля 2020 г.

Почему не возникает ошибка при s[0:len(s)]?

#python #language_lawyer


s = "Worldoftanks"


Почему print(s[0:len(s)]) будет работать?

len(s) вернет 12. Тогда получается s[0:12], а это ошибка: обращение к несуществующему
индексу.
    


Ответы

Ответ 1



Для начала стоит отметить, что все верно. Из документации ( раздел Sequences): a[i:j] вернет все элементы с индексом k таким, что i <= k < j. Обратите внимание на то, что j строго больше. Послединий индекс "Worldoftanks" - 11 (нумерация с нуля) и срез [0:len(obj)] вернет все элементы с индексами 0 1 2 ... 10 11 равенство 11 < 12 выполнено. Однако, есть другой момент: Срезы представляют собой отдельные объекты, которые возвращает функция slice. В определении сказано, что при взятии среза нормальным способом ([start:stop:step]) объекты этого типа используются неявно. Когда необходимо будет вернуть результат среза, будут вычислены допустимые границы (границы будут проверены за вас, да), чтобы не произошло выхода за пределы. За это отвечает метод indices (getIndices в CPython). Можно немного переписать взятие индекса вот так: slic = slice(0, 20) print("Тип объекта из функции slice:", type(slic)) my_string = "Hello world" print("Корректные индексы:", slic.indices(len(my_string))) print("Результат среза", my_string[slic]) >>> Тип объекта из функции slice: >>> Корректные индексы: (0, 11, 1) >>> Результат среза Hello world Как видно, никаких ошибок, ибо indices вернул корректные границы.

Ответ 2



s[0:12] - это ж с нулевой по 11ю; первый индекс - это откужа начинается то, что нам надо, второй - откуда начинается то, что нам НЕ надо; както так в доках поедлагается запоминать, чтобы не путаться ))) UPD: вообще, синтаксис слайсов (они же - "срезы") НЕ вызывает ошибок при выходе за пределы массива/строки, в отличие от обращения к единичному элементу. ну т.е. s[100] вызовет ошибку, а s[100:101] - уже не вызовет, как и s[100:], ну и все такое. В доках об этом явно сказано: docs.python.org/2/tutorial/introduction.html , ищем строчку "However, out of range slice indexes are handled gracefully when used for slicing"

Ответ 3



Почему print(s[0:len(s)]) будет работать? Правая граница среза так же как и для range(len(s)) в Питоне не включается†: >>> [*range(4)] [0, 1, 2, 3] Видно что правая граница 4 исключена. Аналогично, print(s[0:len(s)]) печатает все символы с допустимыми индексами от 0 до len(s) - 1 включительно, то есть в диапазоне 0 <= i < len(s). E.W. Dijkstra объясняет (в 1982) почему правую границу не следует включать. Фактически для строк (str тип) и других стандартных последовательностей в Питоне можно использовать в срезах значения вне допустимых индексов, к примеру: 'a'[-42:1000] не выбрасывает ошибку, так как s[i:j] формально: (4) The slice of s from i to j is defined as the sequence of items with index k such that i <= k < j. If i or j is greater than len(s), use len(s). If i is omitted or None, use 0. If j is omitted or None, use len(s). If i is greater than or equal to j, the slice is empty. (3) If i or j is negative, the index is relative to the end of sequence s: len(s) + i or len(s) + j is substituted. But note that -0 is still 0. Если индексы отрицательны, то к ним прибавляется len(s) и затем s[i:j] равнозначно‡: s[max(0, min(i, len(s))) : max(0, min(j, len(s)))] то есть i,j принудительно к диапазону [0, len(s)] (оба конца включительно) приводятся. Неуказанные границы или None заменяются на 0 и len(s) соответственно: s[0:len(s)] == s[:] == s. Если в итоге i >= j, то пустой срез создаётся. Иллюстрация из вводного руководства: +---+---+---+---+---+---+ | P | y | t | h | o | n | +---+---+---+---+---+---+ 0 1 2 3 4 5 6 -6 -5 -4 -3 -2 -1 "Python"[3:5] == "ho"—визуально достаточно выбрать буквы ('h' и 'o'), которые между указанными индексами (3 и 5) находятся. s[i:j] действует как type(s).__getitem__(s, slice(i, j)). Пример реализации: class FakeSeq: ... def calc_item(self, i): ... def __getitem__(self, item): if isinstance(item, slice): indices = item.indices(len(self)) return FakeSeq([self.calc_item(i) for i in range(*indices)]) else: return self.calc_item(i) slice.indices() метод используется, чтобы помочь реализовать необходимое поведение для среза. К примеру, "a"[-42:1000]: >>> slice(-42, 1000).indices(len("a")) (0, 1, 1) # start, stop, step >>> list(range(*_)) [0] # indices >>> 'a'[-42:1000] == 'a'[0] True Фактически в CPython для строк вызывается похожая функция PySlice_GetIndicesEx(). В общем случае, типы передаваемые в slice() неограничены и их интерпретация зависит от конкретного типа: class Slicable(object): def __getitem__(self, given): if isinstance(given, slice): # do your handling for a slice object: print(given.start, given.stop, given.step) else: # Do your handling for a plain index print(given) s = Slicable() s[1] s[1:2] s[1:2:3] s[1:2:3,4] s[()::1j,] s[()::1j,None] Результат 1 1 2 None 1 2 3 (slice(1, 2, 3), 4) (slice((), None, 1j),) (slice((), None, 1j), None) Реальный пример: кортежи, ... и 1j индексы используются в numpy. †Что значит * (звёздочка) и ** двойная звёздочка в Питоне? ‡Часть об обнулении отрицательных индексов я не нашёл где явно задокументирована (кроме упомянутой цитаты из вводного руководства (неформально): "out of range slice indexes are handled gracefully when used for slicing") и из новостей для Питон 2.3 (древняя версия): "[slice.]indices() handles omitted and out-of-bounds indices in a manner consistent with regular slices (and this innocuous phrase hides a welter of confusing details!"выделение моё Выделенная часть переводится как: «эта безобидная фраза скрывает кучу сбивающих с толку деталей»

воскресенье, 9 февраля 2020 г.

Почему std::find не использует мой operator==?

#cpp #language_lawyer


Я реализовал свою перегрузку operator== для сравнения своего std::pair<...> с std::string.
Но по какой-то причине компилятор не может найти эту перегрузку. С чем это может быть
связано?

Код для воспроизведения ошибки:

#include 
#include 
#include 
#include 

typedef std::pair RegPair;

bool operator==(const RegPair& lhs, const std::string& rhs)
{
    return lhs.first == rhs;
}

int main()
{
    std::vector sequence;
    std::string foo("foo");
    std::find(sequence.begin(), sequence.end(), foo);
}


Текст ошибки:


GNU GCC:


  error: no match for 'operator==' in '__first. __gnu_cxx::__normal_iterator<_Iterator,
_Container>::operator* with _Iterator = std::pair, std::allocator >, int>*, _Container
= std::vector, std::allocator >, int>, std::allocator, std::allocator >, int> > > == __val'

clang:


  error: invalid operands to binary expression ('std::pair, int>' and 'std::basic_string
const')





Данный вопрос является свободным переводом «Why isn't std::find() using my operator==?».
    


Ответы

Ответ 1



Ответ по ссылке, с которой был сделан перевод, неверен/неточен. ADL-поиск никак не заменяет/не исключает обычный поиск, а лишь дополняет его. Правильное описание ситуации заключается в следующем: Обычный поиск выполняется из места вызова оператора == из определения шаблона функции std::find в стандартной библиотеке. Он находит только те имена, которые видны из этого места. Понятно, что оттуда приведенное определение оператора == не видно. ADL-поиск выполняется в ассоциированных и только в ассоциированных пространствах имен и видит эти пространства имен такими, каким они стали на момент вызова функции std::find в вызывающем коде. Набор ассоциированных пространств имен строится в соответствии с правилами, описанными 6.4.2/2. В данном случае из точки вызова std::find приведенное определение оператора == прекрасно видно. Но это определение сделано в глобальном пространстве имен. А ассоциированным для ADL в данном случае является только пространство std, ибо оба аргумента сравнения принадлежат пространству std. Поэтому глобальное пространство имен не рассматривается ADL и данное определение не находится. Утверждение о том, что ADL якобы прекращает дальнейший поиск определений operator == именно из-за того, что какие-то определения operator == уже найдены внутри std - неверно. ADL всегда ищет имена только внутри ассоциированных пространств имен. В отличие от обычного lookup, ADL никогда не расширяет область поиска за пределы ассоциированных пространств имен, независимо от того, найдено там что-либо или нет. Ту же проблему можно проиллюстрировать следующим маленьким примером namespace N { struct S {}; } template void foo(T a) { bar(a); // 1 } void bar(N::S s) {} int main() { N::S a; foo(a); // 2 } При таком порядке объявлений обычный поиск имен находит имена, видные из точки 1, а ADL поиск находит имена, видные из точки 2, но только в ассоциированных пространствах имен. Глобальное пространство имен ассоциированным не является, поэтому объявление void bar(N::S s) не находится и код не компилируется. В исходном варианте, если мы каким-то образом "притянем за уши" глобальное пространство имен в качестве ассоциированного для ADL, то данный оператор == сразу начнет находиться через ADL. Например, объявим в глобальном пространстве имен некий фиктивный тип, приводимый к std::string и используем именно его в качестве ключа для поиска ... struct S : std::string { using std::string::string; }; int main() { std::vector sequence; S foo("foo"); std::find(sequence.begin(), sequence.end(), foo); } Определение оператора сравнения при этом менять не надо. Код сразу начнет компилироваться и использовать данный оператор сравнения. Другой вариант внешне "невинной" замены, который заставит код компилироваться - сделать второй член пары типом из глобального пространства имен struct X {}; typedef std::pair RegPair; Больше ничего менять не надо - этого уже достаточно для того, чтобы ассоциировать глобальное пространство имен для ADL.

Ответ 2



Проблема заключается в том, что std::find является шаблонной функцией, а потому при поиске operator== в дело вступает ADL (поиск, зависимый от типов аргументов). Оба аргумента функции (std::pair и std::string) находятся в одном и том же пространстве имён (::std), поэтому ADL начинает поиск именно в нём. При этом достаточно, чтобы там был определён хоть какой-то operator==, так как сопоставление имён (name lookup) производится до определения подходящих перегрузок. В связи с тем, что подходящий по имени operator== обязательно будет найден (в том же объявлен как минимум оператор сравнения std::string с чем-то), алгоритм останавливает свою работу. До вашей же перегрузки, расположенной в глобальном пространстве имён (то есть за пределами ::std), очередь так и не дойдёт. Данное сообщение является свободным переводом ответа участника James McNellis.

Неявная декларация

#c #language_lawyer


Подскажите, пожалуйста, если в a.c нет объявления функции, которая определена в b.c,
но при этом в a.c происходит вызов такой функции, то что происходит? 

Интересует язык C.

В описанной ситуации MinGW-w64 говорит:

warning: implicit declaration of function '...'


Я так понимаю, что в этом случае происходит неявная декларация (объявление) функции?

Правильно ли я понимаю, что код работает только потому, что автоматически сформированная
сигнатура совпадает с реальной сигнатурой? Если она не совпадет, то это неопределенное
поведение?

Хочу разобраться подробнее.
    


Ответы

Ответ 1



К современному С эта тема имеет отношение только в контексте вызова функции, объявленной без прототипа. Современный язык С запрещает вызов необъявленных функций. Начиная с C99 все функции должны быть объявлены пред вызовом. Вызов необъявленной функции - это constraint violation, т.е. ошибка. Все остальное - самодеятельность вашего компилятора, к языку С никакого отношения не имеющая. В устаревшем стандарте C89/90 вызов необъявленных функций разрешался. Компилятор пытался "угадать" тип функции на основе количества и типов передаваемых аргументов (типы аргументов после default argument promotions), а также подразумевал возвращаемое значение типа int. short s = 0; /* Вызов необъявленной функции */ foo(s, 5, 3.14f, "H"); /* Компилятор предполагает: int foo(int, int, double, char *) */ Современный С ведет себя по этой же схеме, если объявление функции сделано, но без прототипа (т.наз. "объявления в K&R стиле"). Угадывать тип возвращаемого значения больше не нужно, т.к. он явно присутствует в объявлении, а угадывание типов параметров делается по-старому. Объявления без прототипа являются deprecated. Если "угаданный" тип функции несовместим с фактическим - поведение не определено. При попытке вызова variadic функции без предварительного объявления поведение не определено в любом случае.

пятница, 31 января 2020 г.

Приведите пример использования Volatile.Read,Write

#c_sharp #многопоточность #language_lawyer


Я опробовал в интернете множество примеров,но они не показывают на практике,разницу
работы одного и того-же кода с применением Volatile и без.Можно ли на персональном
компьютере осуществить это,или разницу можно  увидеть только при работе сервера с множеством
клиентов,которые одновременно используют и изменяют одну и туже переменную через разные
потоки?
    


Ответы

Ответ 1



Ну вот вам пример. class Program { static bool finish = false; static void Main(string[] args) { new Thread(ThreadProc).Start(); int x = 0; while (!finish) { x++; } } static void ThreadProc() { Thread.Sleep(1000); finish = true; } } Откомпилируйте Release, запустите из командной строки (не из Visual Studio), увидите, что программа не завершается. Поменяйте код на такой: class Program { static bool finish = false; static void Main(string[] args) { new Thread(ThreadProc).Start(); int x = 0; while (!Volatile.Read(ref finish)) // while (!finish) { x++; } } static void ThreadProc() { Thread.Sleep(1000); Volatile.Write(ref finish, true); // finish = true; } } — этот код будет завершаться. Идея примера честно украдена из ответа Marc Gravell. Пояснение. Многопоточные проблемы с обновлением данных между потоками возникают не только из-за оптимизации на уровне процессора (вид разных процессоров многопроцессорной системы на оперативную память может быть различным), а и из-за оптимизаций компилятора, который имеет право (это важно!) переставлять операции, если только смысл кода в каждом отдельном потоке не меняется. В обычном случае х86-системы с достаточно сильной моделью памяти процессор таких трюков почти не делает, так что проблемы нам с вами видны не так часто, в отличие от программистов на ARM, например. А вот оптимизации компилятора, наоборот, становятся всё агрессивнее и агрессивнее, хотя и в рамках стандарта языка. Право компилятора на изменение внутреннего смысла кода известно как as-if rule: компилятор может производить любой код, если только его побочные эффекты в каждом отдельном потоке оставались те же и в том же порядке. В частности, компилятор имеет право выбросить повторное чтение переменной, если он видит, что она в данном потоке не меняется. Однако некоторые операции (например, Volatile.Read/Write, lock, Thread.Start/Join и т. п.) являются критическими точками, и оптимизации проводятся лишь в блоках между такими точками. В нашем случае оптимизатор смог понять, что переменная finish с точки зрения основного потока не меняется, и выкинул повторное чтение. Имел на это полное право. Когда мы добавили Volatile-операции, то компилятор более не имел права не перечитывать переменную, так что изменения были «замечены». Ссылки на C# Language Specification (раздел 3.10): Execution of a C# program proceeds such that the side effects of each executing thread are preserved at critical execution points. [...] The execution environment is free to change the order of execution of a C# program, subject to the following constraints: Data dependence is preserved within a thread of execution. That is, the value of each variable is computed as if all statements in the thread were executed in original program order. Initialization ordering rules are preserved (§10.5.4 and §10.5.5). The ordering of side effects is preserved with respect to volatile reads and writes (§10.5.3). [...] (раздел 10.5.3): For non-volatile fields, optimization techniques that reorder instructions can lead to unexpected and unpredictable results in multi-threaded programs that access fields without synchronization such as that provided by the lock-statement (§8.12). These optimizations can be performed by the compiler, by the run-time system, or by hardware.

среда, 29 января 2020 г.

Преобразование char -> int цифры и буквы

#cpp #c #стандарт #language_lawyer


Довольно часто в коде можно увидеть строки типа 

char cdigit = '8';
int idigit = cdigit - '0';


Реже, но также встречается

char letter = 'd';
int letter_number = letter - 'a';


Открываем Страуструпа "Язык прогрммирования С++" специальное издание, "Бином-Пресс",
2008. Цитата (стр 110):


  Небезопасно считать, ... что символы алфавита непрерывны (в стандарте EBCDIC между
i и j имеется разрыв)


У Кернигана и Ритчи вышеприведенный код (по крайней мере по цифрам) встречается регулярно.

Собственно вопрос по цифрам: обязательно ли корректен код idigit = cdigit - '0'?
    


Ответы

Ответ 1



В стандарте C++ §2.3/4 гласит: In both the source and execution basic character sets, the value of each character after 0 in the above list of decimal digits shall be one greater than the value of the previous. то есть В обоих наборе символов исходного текста и времени выполнения, значение каждого символа после 0 в приведённом выше списку десятичных цифр должно быть на единицу больше предыдущего. (перевод мой). Приведённый в §2.3/1 набор цифр таков: 0 1 2 3 4 5 6 7 8 9 Это гарантирует последовательность значений кодов цифр.

Ответ 2



Нашел всё-таки такую фразу в сишном Стандарте (C99): In both the source and execution basic character sets, the value of each character after0in the above list of decimal digits shall be one greater than the value of the previous.

Проблема с наследованием интерфейсов

#c_sharp #net #language_lawyer


Почему при компиляции этого:

using System;

public class Test
{
  public static void Main()
  {
    Lol l = new Lol();
    Console.WriteLine(((IParent)l).Family);
    Console.WriteLine(((IChild)l).Family);
    Console.WriteLine(l.Name);
  }
}

public interface IParent
{
  string Family { get; }
}

public interface IChild : IParent
{
  string Name { get; }
}

public class Lol : IChild
{
  string IParent.Family { get { return "suck"; } }
  string IChild.Family { get { return "duck"; } }
  public string Name { get { return "ross"; } }
}


IdeOne выдает это:

prog.cs(27,26): error CS0550: Lol.IChild.Family.get is an accessor not found in interface
member IChild.Family
Compilation failed: 1 error(s), 0 warnings
    


Ответы

Ответ 1



Дело в том, что у класса может быть лишь одна имплементация метода/свойства интерфейса. Интерфейс — лишь «обещание» имплементировать те или иные методы/свойства, а наследованный интерфейс — лишь более сильное обещание. У вас объявление class Lol : IChild есть обещание имплементировать все методы/свойства интерфейса IChild (то есть, в вашем случае, Name), а также все методы/свойства родительского интерфейса IParent. Свойство Family интерфейса IChild унаследовано от интерфейса IParent, таким образом в интерфейса IChild содержится не два, а только одно свойство Family. Разрешение имплементировать как IParent.Family, так и IChild.Family, привело бы к двум различным имплементациям одного и того же свойства. При этом компилятор не знал бы, какое из них использовать. Поэтому строчка string IChild.Family { get { return "duck"; } } не пропускается компилятором. Формально, запрещение можно найти в спецификации языка (которая находится в каталоге <тут каталог Visual Studio>\VC#\Specifications\1033\CSharp Language Specification.docx), в разделе 13.4.1 Explicit interface member implementations. Там разобран именно ваш случай: The fully qualified name of an interface member must reference the interface in which the member was declared. Thus, in the declarations interface IControl { void Paint(); } interface ITextBox: IControl { void SetText(string text); } class TextBox: ITextBox { void IControl.Paint() {...} void ITextBox.SetText(string text) {...} } the explicit interface member implementation of Paint must be written as IControl.Paint.

пятница, 24 января 2020 г.

Чем плох %n в printf?

#cpp #c #безопасность #инспекция_кода #language_lawyer


http://ideone.com/ffjDAv

#include 

char *names = "Windows\0System\0Config\0";

int main() {
  int l, r;

  for (char *name=names; *name; name+=r-l+1)
    if (printf("Folder: %n%s%n\n", &l, name, &(r=0)), !r)
      break; // Произошла ошибка, вероятно, стоит что-то сделать

  return 0;
}


Понятно, что при скармливании printfу пользовательских строк в качестве формата,
%n может сделать что-то нехорошее. Но есть ли от него вред в подобном коде по сравнению
с вариантом, в котором он не используется?

http://ideone.com/xd5SYu

#include 
#include 

char *names = "Windows\0System\0Config\0";

int main() {
  for (char *name=names; *name; name+=strlen(name)+1)
    printf("Folder: %s\n", name);

  return 0;
}


PS: На основе обсуждения в другом ответе.
    


Ответы

Ответ 1



Если потом вместо printf кто-нибудь напишет wprintf - то первый код сломается для строк, содержащих национальные символы.

border-radius у body

#html #css #language_lawyer #border #overflow


Почему div не обрезался до круга? В аналогичном случае с другим контейнером всё обрезается.
Причём, вроде как во всех браузерах такое поведение.



body {
  width: 10em;
  height: 10em;
  border-radius: 50%;
  overflow: hidden;
}

div {
  height: 100%;
  width: 100%;
  background: silver;
}
main { width: 10em; height: 10em; border-radius: 50%; overflow: hidden; } div { height: 100%; width: 100%; background: silver; }


Ответы

Ответ 1



Хороший вопрос. Мне кажется дело в том, что body не может применить на себе некоторые свойства. Если хочешь все div закруглить: * div { border-radius: 50%; } div { width: 20px; height: 20px; background: black; color: white; padding: 50px; }
1
2
3
4


четверг, 23 января 2020 г.

Неопределенное поведение при определении/объявлении внешних глобальных констант

#cpp #language_lawyer


В книге "Программирование: принципы и практика использования С++" приводится следующий
пример:

// file f1.cpp
int x1 = 1;
int y1 = x1+2; // y1 becomes 3


 // file f2.cpp
extern int y1;
int y2 = y1+2; //UB, y2 becomes 2 or 5


То есть неизвестно, в каком порядке будут инициализированы глобальные переменные.
Стоит ли ожидать аналогичного поведения при использовании глобальных констант?:

// file f1.cpp
int x1 = 1;
const int y1 = x1+2; // y1 becomes 3


 // file f2.cpp
extern const int y1;
int y2 = y1+2; //UB???


И почему UB возникает вообще? Ведь переменная/константа во втором файле явно объявлена,
но не инициализирована, а значит чтобы "узнать" её значение нужно обратиться к определению,
а в этом случае y1 явно = 3.
Ведь когда программа вызывает функцию, которая объявлена в первом файле, но определена
во втором, она ведь не предполагает, что тело функции пусто (равно нулю), как это происходит
в случае с внешними глобальными переменными.
    


Ответы

Ответ 1



Во-первых, вы должны в файле f1.cpp указать, что константа имеет внешнее связывание extern const int y1 = x1+2; // y1 becomes 3 иначе она будет не видна в файле f2.cpp, так как константы в C++ имеют внутреннее связывание. Что касается вашего вопроса, то это не имеет значения, является ли переменная константой или нет. Проблема связана с тем, что не определен порядок инициализации статической памяти у модулей. Что касается функций, то редактор связей просто проставляет адреса для внешних символов. К самим же функциям обращение идет во время выполнения. Поэтому, например, статические переменные функции инициализируются при вызове функции.

среда, 22 января 2020 г.

Нужно ли писать пустой виртуальный деструктор?

#cpp #наследование #language_lawyer #неопределенное_поведение


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

Но что если лично мне вообще не нужен деструктор ни в одном из классов?
Являются ли автоматически сгенерированные деструкторы взаимозаменяемыми?
Или я всё равно обязан добавить виртуальный деструктор в родительский класс?

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

В таком случае кажется странным необходимость добавлять виртуальную функцию, которая
будет вызываться, если эта функция заведомо является nopом.

https://ideone.com/8ldKsU

struct A {
  int x;
  A(unsigned x) : x(x) {}
};

struct B : A
{
  B() : A(7) {}
};

int main()
{
  A *a = new B();
  delete a;

  return 0;
}

    


Ответы

Ответ 1



Ответ на этот вопрос по сути совпадает с этим ответом. Виртуальным деструктор может стать только при наследовании или явном указании, больше никак. Компилятор генерирует деструктор, который должен удалить все объекты, т.е. «пустой» это не верно в общем случае. В стандарте нет исключений для этого случая, код в вопросе даёт неопределённое поведение согласно стандарту. Дополнительно, пустой с точки зрения исходного кода деструктор на деле не является пустым, в него вкладывается логика по деаллокации экземпляра класса. Поэтому деструкторы разных классов по сути являются разными, не взаимозаменяемыми.

Ответ 2



Как известно, при необходимости удаления через указатель на родительский класс, класс должен иметь виртуальный деструктор, чтобы оператор delete вызвал верный деструктор дочернего класса. Если дело дошло до виртуальных функций и до удаления объекта по указателю на базовый класс, то рекомендуется писать виртуальные деструкторы, хотя бы и пустые. Потом меньше возни будет их добавлять. Опять же любая программа имеет тенденцию стать библиотекой и в таком качестве следующие пользователи начинают производить свои классы от Ваших классов. Тут и пригодится виртуальный деструктор. Что касается приведенного примера, то в нем нет ни виртуальных функций, ни НЕОБХОДИМОСТИ удалять объект класса по по указателю на базовый класс. В качестве иллюстрации того, что в С++ МОЖНО обойтись и без виртуальных деструкторов такой пример имеет право на существование. Но в реальном коде с более-менее развитой системой иерархии классов виртуальные деструкторы must have.

суббота, 11 января 2020 г.

Что если обернуть тип массива в скобки при выделении памяти?

#cpp #language_lawyer #delete


Стандартный способ выделить массив из 5 элементов - так:

int *a = new int[5];


ну и удаляется он

delete[] a;


А что если добавить скобки?

int *b = new (int[5]);


Круглые скобки заставляют выделять один объект, являющийся массивом из 5 элементов,
а не массив из 5 элементов. Это подтверждается сообщением об ошибке при попытке подставить
переменную в размер массива (где d=):

http://codepad.org/BvHyTPs6

#include 

using namespace std;

int main()
{
  int *a = new int[5];
  int *b = new (int[5]);

  int n = 5;
  int *c = new int[n];
  int *d = new (int[n]);

  return 0;
}



In function 'int main()':
Line 12: error: ISO C++ forbids variable-size array
compilation terminated due to -Wfatal-errors.



Чем массив b отличается от массива a и как правильно освободить за собой память,
выделенную таким образом?

delete b;


или всё-таки

delete[] b;


и почему?

А может через такой указатель вообще удалять нельзя, потому что его тип отличается
от типа выделенного объекта?

Кстати, на ideone аналогичный код компилируется, как и обычный variable-size array:
https://ideone.com/MGmBya. Этот факт как-то влияет на освобождение памяти?



Также выяснилось, что компилятор не хочет выводить константность размера из такого
объявления https://ideone.com/XwtTMQ & http://codepad.org/p3VKqMvd

#include 

using namespace std;

template  void f(int a[n])
{
  cout << n << endl;
}

int main()
{
  f(new (int[5]));
  return 0;
}



prog.cpp: In function ‘int main()’:
prog.cpp:12:17: error: no matching function for call to ‘f(int*)’
   f(new (int[5]));
                 ^
prog.cpp:5:26: note: candidate: ‘template void f(int*)’
 template  void f(int a[n])
                          ^
prog.cpp:5:26: note:   template argument deduction/substitution failed:
prog.cpp:12:17: note:   couldn't deduce template parameter ‘n’
   f(new (int[5]));
                 ^



Тогда возникает вопрос, почему та же конструкция с переменной - это VLA, а с константой
- простой указатель.


    


Ответы

Ответ 1



Ключевой момент, как уже заметил @VTT в комментариях: Круглые скобки заставляют выделять один объект, являющийся массивом из 5 элементов, а не массив из 5 элементов. это неправда. #include int main() { int ptr[5] = {0, 1, 2, 3, 4}; auto p1 = new (ptr)(int[5]); auto p2 = new (int[5]); std::cout << p1[3] << std::endl; delete []p2; // ok delete []p1; // error return 0; } new expression - вариант синтаксиса (1), с указанием типа в скобках То есть вы по прежнему выделяете массив из 5 элементов. ::(optional) new (placement_params)(optional) ( type ) initializer(optional) вот описание синтаксиса new expression, собcтвенно вы указываете тип в скобках. Вот здесь: auto p2 = new (int[5]); Типом будет массив из 5 элементов. Это то же самое что написать auto p2 = new int[5]; только это уже второй вариант синтаксиса, когда тип указывается без скобок. И во втором варианте (без скобок) вы можете в качестве первой размерности задавать переменную, которую можно привести к std::size_t, а в первом варианте так делать уже нельзя. То есть можно: int n = 5; auto p = new int[n][5]; Но нельзя int n = 5; auto p = new int[5][n]; auto p1 = new (int[n][5]); If type is an array type, all dimensions other than the first must be specified as positive integral constant expression (until C++14)converted constant expression of type std::size_t (since C++14), but (only when using un-parenthesized syntax (2)) the first dimension may be any expression convertible to std::size_t. This is the only way to directly create an array with size defined at runtime, such arrays are often referred to as dynamic arrays

Ответ 2



Компилятор смотрит тип предоставленный оператору new и если этот тип является массивом, то возвращаемое значение будет указателем на первый элемент. int * ip = new int[1]; ip имеет тип указателя на int, а тип int[1] - это уже массив. Второй случай, где оператору new предоставлен тип не массив: char * cp = new char ; Здесь cp это указатель на char, а аргумент оператору тоже char. Чтобы определить, как удалять память нужно сравнить типы *указатель и аргумент new. При сложном случае, когда типы неизвестны (typedef или шаблон) можно воспользоваться сравнением типа с помощью typeid. // g++ -Wall -Wextra -Wpedantic -Os -std=c++11 newarr3.cpp #include #include using namespace std; class C{ public: int i; C(){cout<<"C;";} ~C(){cout<<"~C;";} }; typedef C A[5]; typedef C B[1] ; typedef C D ; int main() { cout<<"auto ap = new A :"< "<< "typeid(A).name = "< "<< "typeid(B).name = "< "<< "typeid(D).name = "< typeid(A).name = A5_1C ~C;~C;~C;~C;~C; C; typeid(*bp).name = 1C <> typeid(B).name = A1_1C ~C; C; typeid(*dp).name == typeid(D).name == 1C ~C; В вашем примере однозначно видны типы. Возвращаемый тип это указатель на int, а память выделяется на тип массива. Удалять нужно с помощью delete[].

пятница, 10 января 2020 г.

Конструктор по умолчанию

#cpp #классы #перегрузка_операторов #конструктор #language_lawyer


class A
{
  public:
   A():a(0),b(0) {}
   explicit A(int x): a(x), b(0) {}
   A(int x, int y): a(x), b(y) {}
  private:
   int a,b;
};


и

class A
{
  public:
   explicit A(int x=0, int y=0): a(x), b(y) {}
  private:
   int a,b;
};


Есть ли различия? Что лучше использовать?
    


Ответы

Ответ 1



Эти два объявления классов не эквивалентны. Во втором объявлении класса конструктор объявлен со спецификатором функции explicit, а это ограничивает применение этого конструктора в различных ситуациях. В первом же объявлении класса только конструктор преобразования объявлен со спецификатором функции explicit. А это означает, что другие конструкторы вы можете вызывать неявно. То есть первое объявление предоставляет больше возможностей по использованию класса. Рассмотрите следующую демонстрационную программу #include struct A { explicit A( int x = 0, int y = 0 ) : x( x ), y( y ) {} int x; int y; }; struct B { B() : x( 0 ), y( 0 ) {} explicit B( int x ): x( x ), y( 0 ) {} B( int x, int y ): x( x ), y( y ) {} int x; int y; }; void f( const A &a ) { std::cout << "a.x = " << a.x << ", a.y = " << a.y << std::endl; } void g( const B &b ) { std::cout << "b.x = " << b.x << ", b.y = " << b.y << std::endl; } int main() { // f( {} ); // f( { 1, 2 } ); g( {} ); g( { 1, 2 } ); } Ее вывод на консоль: b.x = 0, b.y = 0 b.x = 1, b.y = 2 В этой программе два вызова функции f закомментированы, так как если их раскомментировать, то компилятор выдаст сообщение об ошибке. Другое важное отличии состоит в том, что один класс имеет всего лишь один конструктор с заданной сигнатурой, а другой класс имеет три конструктора с различными сигнатурами. Рассмотрите еще один демонстрационный пример struct A { explicit A( int x = 0, int y = 0 ) : x( x ), y( y ) {} int x; int y; }; struct B { B() : x( 0 ), y( 0 ) {} explicit B( int x ): x( x ), y( 0 ) {} B( int x, int y ): x( x ), y( y ) {} int x; int y; }; struct C { //friend A::A(); friend B::B(); }; int main() { } Здесь в классе C вы можете объявить конструктор по умолчанию класса B в качестве друга класса С. Однако вы не можете сделать то же самое с конструктором по умолчанию класса A, чтобы объявить его другом класса C, так как конструктор по умолчанию в классе A имеет другую сигнатуру. Вам уже придется писать struct C { friend A::A( int, int ); }; а это может быть не тем, что вы хотели бы получить. То есть если вы, например, хотели, чтобы другом был конструктор, который вызывается исключительно без аргументов. То есть, опять-таки, когда имеются отдельные конструкторы, то ваши возможности более широкие. Если рассматривать не конструкторы, а функции, то разница имеется еще более существенная. Аргументы по умолчанию не влияют на тип функции. Поэтому, например, если вы объявили функцию как void f( int, int = 0 ); то, несмотря на аргумент по умолчанию и того факта, что вы можете ее вызывать как f( value ); тем не менее ее тип void( int, int ). А это в свою очередь означает, что вы не можете, например, написать void h( void f( int ) ) { f( 10 ); } void f( int x, int y = 0 ) { std::cout << "x = " << x << ", y = " << y << std::endl; } // h( f ); так как параметр функции h имеет тип void( int )., а у функции, используемой в качестве аргумента, тип void( int, int ) Если же вы объявите две функции вместо одной void h( void f( int ) ) { f( 10 ); } void f( int x ) { std::cout << "x = " << x << std::endl; } void f( int x, int y ) { std::cout << "x = " << x << ", y = " << y << std::endl; } то данный вызов h( f ); будет корректным, так как имеется функция с одним параметром.

Ответ 2



Различия уже объяснил @Vlad from Moscow, я лишь предложу, из двух вариантов в вопросе, третий вариант: class A { public: A():A(0, 0) {} explicit A(int x): A(x, 0) {} A(int x, int y): a{x}, b{y} {} private: int a,b; }; На мой взгляд именно этот вариант является лучшим, т.к. он имеет явный конструктор с одним аргументом, что является хорошей практикой и уберегает от некоторых ошибок. С другой стороны, explicit для конструкторов, у которых больше или меньше одного аргумента, на мой взгляд, является лишним. Т.к. случайно создать объект из более чем одного аргумента проблематично, а именно за этим мы и приписали explicit у одноаргументнова конструктора — защита от случайных ошибок. Ну и самое главное, мы имеем лишь один конструктор, который инициализирует поля. Все остальные действуют через него, что позволяет минимизировать ошибки инициализации.

понедельник, 6 января 2020 г.

delete [] и перемещения

#cpp #language_lawyer


int* a = new int[4];
++a;
--a;
delete [] a;


Будет валидным такой код?

А вот такой:

int* a = new int[4];
++a;
delete [] a;


Необходимо просто что бы какой нибудь указатель указывал на начало динамического
выделенного массива? И как распознается длинна массива которую нужно уничтожить? 

Может быть по типу? Но указатель указывает только на первый элемент в массиве, int*,
а не ( * a)[4]. И как можно привести во втором коде int* к (*a)[4]? Почему нет неявного
приведения?
    


Ответы

Ответ 1



Данный фрагмент кода совершенно корректный, так как оператор delete использует значение, хранящееся в указателе, которое равно значению, ранее полученному с использованием оператора new. int* a = new int[4]; ++a; --a; delete [] a; Для оператора совершенно неважно, какое до этого в указателе было значение, и как оно изменялось. Чтобы это сделать еще более наглядным то вы можете написать, например, int x; int *px = &x; int* a = new int[4]; px = a; a = &x; delete [] px; Однако данный фрагмент кода int* a = new int[4]; ++a; delete [] a; некорректный, так как указатель a не содержит адрес выделенной динамически памяти. delete и free должны применятся к значению указателя полученому по new или malloc: это требование стандарта. Обычно "под капотом" оператора new делается следующее. К запрашиваемому размеру памяти, который вы хотите выделить, добавляется к началу блока префикс, который будет хранить этот размер блока памяти. И адрес этого префикса просто вычисляется как смещение на размер слова (обычно sizeof( int) ) от начала участка памяти, адрес которой возвращается пользователю оператора new. Это значение используется, чтобы при удалении памяти правильно оценить ее размер. Что касается вашего второго вопроса, то вы можете написать, например, int* a = new int[4]; int ( *p )[4] = reinterpret_cast( a );

воскресенье, 5 января 2020 г.

Чтение и запись членов union

#cpp #c #language_lawyer


Никак не могу найти однозначный ответ на следующий вопрос.

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

Например:

typedef union u_color_pack
{
    uint8_t b[4];
    uint32_t raw;
} color_pack;

// …

uint8_t color_check(const uint32_t _raw)
{
    color_pack color;
    color.raw = _raw;
    if (color.b[0] == 0)
    {
        return 0;
    } 
    return 1;
}


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

Так вот, проблема в том, что некоторые говорят, что нельзя читать из union-а данные
типа A, если перед этим в union были записаны данные типа B.

Одни говорят, что такая ситуация является неопределенным поведением. Другие - что
это поведение, определяемое реализацией.

Что по этой проблеме говорят стандарты C и C++? Отличается ли то, что они говорят?
    


Ответы

Ответ 1



Задача обращатся по одному адресу к данным и как к целой структуре и как к массиву байт - очень часто-встречаемая задача. из комментариев от VTT, что б избежать UB следует делать так. uint8_t color_check(const uint32_t _raw) { uint_8_t * b = reinterpret_cast(&raw) if (b[0] == 0) { return 0; } return 1; } Не следует делать через union - т.к. UB -оптимизатор предполагает что за одну "итерацию" юнион использует одну ветвь, и при включеном оптимизаторе нового поколения программа может отработать не верно. Через каст без указания типа каста. ((char*)&_raw)[0] - можно поймать UB. Про порядок байт - порядок байт может меняться, только если используются нестандартные платформы, т.е. если вы предполагаете, что код будет работать на платформах отличных от intel x86/x64-совместимых (или там AMD). Для ускорения вычислений - делают предопределение #define и назначают значение предпроцессору, например #ifdef litte_indian // Прямой порядок #else // Обратный порядок #endif Теперь касательно стандартов. На счёт UB https://habr.com/ru/post/216189/ п 1.3.12 Неопределенное поведение (undefined behavior)– поведение, которое может возникать в результате использования ошибочных программных конструкций или некорректных данных, на которые Международный Стандарт не налагает никаких требований. Неопределенное поведение также может возникать в ситуациях, не описанных в Стандарте явно. На счёт использования union, стандарт с не оговаривает как правильно использовать юнион, а стандарт c++ вам уже ответили VTT п 12.3 В с++ union в любой момент времени может быть активно не более одного поля. За исключением доступа к общей подструктуре standard-layout объектов, доступ к неактивным полям является неопределенным поведением. В общем случае для обращения к неактивному полю сначала следует вручную вызвать деструктор активного поля, затем вызвать placement new поля, которое требуется сделать активным.

Ответ 2



В С++ в union в любой момент времени может быть активно не более одного поля. За исключением доступа к общей подструктуре standard-layout объектов, доступ к неактивным полям является неопределенным поведением. В общем случае для обращения к неактивному полю сначала следует вручную вызвать деструктор активного поля, затем вызвать placement new поля, которое требуется сделать активным. 12.3 Unions [class.union] 1 In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended (6.8). At most one of the non-static data members of an object of union type can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time. [Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (12.2), and if a non-static data member of an object of this standard-layout union type is active and is one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of the standard-layout struct members; see 12.2. —end note ]

суббота, 4 января 2020 г.

Деструктор производного класса

#cpp #language_lawyer #деструктор


Следует ли объявлять деструктор производного класса виртуальным, если в базовом классе
он уже помечен таковым? Т.е., необходимость в виртуальном деструкторе в базовом классе
мне ясна, в производном - нет. Возможно, не задумывался бы об этом, если бы не натыкался
на статьи, в которых даны примеры, где по мнению авторов наличие виртуальных деструкторов
в производных классах является, видимо, хорошим тоном.
    


Ответы

Ответ 1



Согласно стандарту C++ (7.1.2 Function specifiers) 5 The virtual specifier shall be used only in the initial declaration of a non-static class member function; То есть спецификатор функции virtual обязан присутствовать только в первоначальном объявлении функции. Тем не менее я соглашусь, что присутствие этого спецификатора в объявлениях функций в производных классах делает код более ясным и самодокументируемым. Что касается деструкторов, то, опять-таки, согласно стандарта C++ (10.3 Virtual functions) 6 Even though destructors are not inherited, a destructor in a derived class overrides a base class destructor declared virtual; see 12.4 and 12.5. То есть если деструктор в базовом классе объявлен со спецификатором virtual, то деструктор в производном классе переопределяет деструктор базового класса, то есть ведет себя как виртуальная функция.

Ответ 2



Он автоматически будет виртуальным. Достаточно пометить функцию-член как виртуальную в базовом классе; такой она будет и в производном. #include using namespace std; class B { public: virtual void f() const { cout << "B\n"; } virtual ~B() { cout << "~B\n"; } }; class C: public B { public: void f() const { cout << "C\n"; } ~C() { cout << "~C\n"; } }; class D: public C { public: void f() const { cout << "D\n"; } ~D() { cout << "~D\n"; } }; int main() { B*b = new D; b->f(); delete b; } Хотя D::f() не имеет слова virtual, это ничего не меняет... Код тут: http://ideone.com/mqG4NW

Ответ 3



Как и все остальные переопределенные виртуальные функции, его надо помечать как override, virtual при этом писать не нужно. struct Derived : Base { ~Derived() override; };

понедельник, 30 декабря 2019 г.

Можно ли игнорировать пустого наследника при написании деструктора?

#cpp #наследование #language_lawyer #неопределенное_поведение


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

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

Пример кода: https://ideone.com/B3Wx7m

struct A {
  int *p;
  A(unsigned n) : p(new int[n]) {}
  ~A() { delete [] p; }
};

struct B : A
{
  B() : A(4) {}
};

int main()
{
  A *a = new B();
  delete a;

  return 0;
}

    


Ответы

Ответ 1



В стандарте четко сказано, что такое удаление вызывает неопределенное поведение. Не делается никаких оговорок на тему того, добавлены ли какие-то поля в класс-наследник или нет. 5.3.5 Delete 3 In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. [...] На деструктор класса также обычно накладываются (могут накладываться) какие-то неявные служебные функции, которые могут работать неправильно при вызове неправильного деструктора. В частности, в типичной реализации на деструктор возложена обязанность вызова правильной функции operator delete, которая может быть перегружена для каждого класса отдельно. То есть если бы в struct B была определена своя статическая функция operator delete, то без виртуального деструктора обеспечивать ее функциональность пришлось бы какими-то другими методами. (Авторы стандарта, скорее всего, и ориентировались именно на такую реализацию вызова перегруженного operator delete.) Например #include struct A { // virtual ~A() {} }; struct B : A { static void *operator new(size_t s) { return (char *) std::malloc(s * 2) + s; } static void operator delete(void *p, size_t s) { std::free((char *) p - s); } }; int main() { A* p = new B; delete p; } В популярных реализациях такой код упадет из-за вызова неправильного operator delete. Но достаточно добавить в A виртуальный деструктор, как все станет хорошо. Обратите внимание, что оба члена B - статические функции, то есть физически в B ничего не добавлено.

воскресенье, 29 декабря 2019 г.

Определение типа захватываемой лямбдой переменной

#cpp #lambda #language_lawyer


#include 
#include 


int main() {
    int x, &y = x;
    [=] {
        std::cout << std::is_same_v
                  << std::is_same_v
                  << std::is_same_v;

        std::cout << std::is_same_v
                  << std::is_same_v
                  << std::is_same_v;
    }();
}


Компилятор gcc выводит 001010, а clang - 001001. Какой вариант правильный и почему?
    


Ответы

Ответ 1



Достаточно запутанный вопрос, но похоже что clang прав. Для начала рассмотрим, как определяется тип выражения e в decltype(e). dcl.type.decltype#1: For an expression e, the type denoted by decltype(e) is defined as follows: if e is an unparenthesized id-expression naming a structured binding, decltype(e) is the referenced type as given in the specification of the structured binding declaration; otherwise, if e is an unparenthesized id-expression naming a non-type template-parameter ([temp.param]), decltype(e) is the type of the template-parameter after performing any necessary type deduction ([dcl.spec.auto], [dcl.type.class.deduct]); otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access, decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed; otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e; otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e; otherwise, decltype(e) is the type of e. x и y являются glvalue, но не являются xvalue, а значит являются lvalue в соответствии с basic.lval#fig:categories: Таким образом, срабатывает предпоследний пункт определения типа, то есть варианты с int отпадают сразу. Далее, рассмотрим следующую цитату. expr.prim.lambda.capture#11: Every id-expression within the compound-statement of a lambda-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type. [ Note: An id-expression that is not an odr-use refers to the original entity, never to a member of the closure type. However, such an id-expression can still cause the implicit capture of the entity. — end note ] ... В данном случае выражения не являются потенциально вычисляемыми (т.к. являются операндами decltype), а значит соответствующие переменные не являются odr-использованными. Однако важно примечание - такие выражения всё ещё могут рассматриваться в контексте лямбда-замыканий, что подтверждается следующей цитатой. expr.prim.id.unqual#2: ... If the entity is a local entity and naming it from outside of an unevaluated operand within the declarative region where the unqualified-id appears would result in some intervening lambda-expression capturing it by copy ([expr.prim.lambda.capture]), the type of the expression is the type of a class member access expression ([expr.ref]) naming the non-static data member that would be declared for such a capture in the closure object of the innermost such intervening lambda-expression. [ Note: If that lambda-expression is not declared mutable, the type of such an identifier will typically be const qualified. — end note ] ... Другими словами, несмотря на то что захвата не происходит, тип выражения определяется так, как будто бы захват есть. Также в примечании сказано про константность идентификатора, если лямбда не имеет спецификатора mutable, что подтверждается следующим пунктом. expr.prim.labmda.closure#4: The function call operator or operator template is declared const ([class.mfct.non-static]) if and only if the lambda-expression's parameter-declaration-clause is not followed by mutable. ... Интересно, что из-за нечёткости формулировок и разбросанности их по разным разделам стандарта (на мой взгляд) оба компилятора имеют соответствующие багрепорты: gcc, clang. Однако в clang репорт закрыт как неверный (RESOLVED INVALID), а в gcc открыт до сих пор (UNCONFIRMED).

Ответ 2



При захвате по значению [=] переменные захватываются копированием. Для каждой захваченной сущности создаётся безымянный член данных внутри лямбда объекта. Для ссылок на объекты тип получается тоже ссылочный: The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise. Так как уточнения о том, какой должна быть ссылка: const или не-const нет, оба рассмотренных компилятора дают подходящий под стандарт результат. Однако, известно, что для модификации, захваченных по значению сущностей нужно дополнительно помечать лямбду как mutable. И если это сделать, можно увидеть, что оба компилятора уже будут давать одинаковые результаты (clang, gcc): #include #include int main() { int x = 42, &y = x; [=]() mutable { std::cout << std::is_same_v << std::is_same_v << std::is_same_v; x = 0; y = 100500; }(); std::cout << "\n" << x << "\n"; } 010 42 Так как требуется явная модификация y = 100500, то тип y уже не может быть константной ссылкой и становится обычной ссылкой. Модифицируются же по-прежнему копии внутри лямбды.

Ответ 3



Ваш пример практически повторяет пример из C++17 стандарта языка. void f3() { float x, &r = x; [=] { // x and r are not captured (appearance in a decltype operand is not an odr-use) decltype(x) y1; // y1 has type float decltype((x)) y2 = y1; // y2 has type float const& because this lambda is not mutable and x is an lvalue decltype(r) r1 = y1; // r1 has type float& (transformation not considered) decltype((r)) r2 = y2; // r2 has type float const& }; } Форма decltype((x)) не является odr-use для x, т.е. она не вызывает неявного захвата x, но она должна вести себя так, как будто x было захвачено и decltype((x)) ссылается именно на захваченное x. При захвате ссылки по значению происходит захват по значению именно ссылаемого объекта. Таким образом никакой разницы в способах x и y захвата в вашем примере нет. Ваши захваченные x и y имеют тип int. Внутри тела не-mutable лямбды они видны как lvalues типа const int. Т.е. в контексте тела вашей лямбды и decltype((x)) и decltype((y)) дают тип const int &. Однако эта часть стандарта подвергается переработке для C++20. Возможно, что есть изменения.