Мысль правильная, но пример, скажем прямо, не показательный и к производительности std::array прямого отношения не имеющий вообще. В реальном коде подобные optimization opportunities, понятное дело, не встречаются. Если, конечно, вы их нарочно не создаете.
… которая вполне успешно используется в STL в функции begin, например.
Где и как именно в функции begin используются "исключения из "эквивалентности" int * и int []?
Наверное потому, что мой компилятор (Intel C++ Compiler) здесь выдаёт true, а не false.
Ну уж я не знаю, как мог Intel C++ Compiler так опростоволоситься. У меня нет под руками живого Intel C++ Compiler, но разглядывание ассемблера от Intel C++ Compiler на godbolt показывает, что для этих типов он генерирует раздельные type_info объекты...
Видимо мне надо более впрямую выражать свои мыслои, а не заниматься дипломатичным хождением вокруг да около.
Поэтому еще раз: тип int (&)[100] не имет ничего общего с типами int * и int [100], поэтому ваш пример с f4 никакого отношения к вопросу эквивалентности int * и int [100] не имеет. Т.е. написана ерунда.
Далее: да, в языке С и С++, если типы T1 И T2 эквивалентны, то и типы T1 * И T2 * тоже эквивалентны.
Но вы, очевидно, запутались в природе "эквивалентностей" наблюдаемых в списках параметров функций. На самом деле в языках С и С++ типы T [N] и T * эквивалентными не являются и никогда не являлись. "Эквивалентность" о которой идет речь в данном случае — не более чем следствие неявной замены типа T [N] на тип T * в процессе интерпретации объявления функции языком. Эта подмена — четко выделенный и оговоренный в спецификациях этих языков отдельный шаг процесса такой итерпретации, а не следствие какой-то врождленной натуральной "эквивалентности" T [N] и T *.
Еще раз напомню, кстати, что даже в параметрах функций коррекность обявления типа T [N] проверяется еще до выполнения вышеупомянутой подмены, то есть при неполном типе T и/или неположительном значении N вы получите ошибку в языке С, в то время как проблем с T * не было бы. Т.е никакой полной "эквивалентности" тут нет.
А уж откуда вы взяли бред про typeid(int[100]) == typeid(int[]) == typeid(int*) — ума не приложу. В языке С++ все эти typeid дают попарно неравные результаты
В моем комментарии нигде не сказано, что вариант с "кэшированием" результатов умножения будет каким-то образом работать быстрее. Абстрактный паттерн в данном случае действиельно "оптимизания через препреоцессинг", но работать это в данном конуретном случае будет скорее всего медленнее, о чем ясно сказано в моем комментарии. Поэтому мне не понятно, зачем вы разражаетесь в ответ исследованиями на тему скорости работы такого подхода.
Речь идет совсем не о скорости.
Целбю моего комментария является лишь демонстрация того факта, что физичекская организация хранения собственно полезных данных матрицы в обоих случаях не должна принципиально отличаться. Отличие, на которое вы напирали с самого начала статьи — лишь побочный эффект навязанного вами же способа выделения памяти. Ни более, ни менее.
С одной стороны, я не совсем понимаю, какое отношение вариант f4 имеет к вопросу эквивалентности (или неэквивалентности) int [] и int *. В этом варианте параметр имеет тип int (&)[100], а это не int [] и не int *. Поэтому как это может быть "одним случаем, когда int[] и int * не эквивалентны" — в упор не ясно.
Если говорить именно об int [], то никакой неэквивалентности быть не может.
С другой стороны, если говорить об общем случае типа T [N] в списке параметров функции, то определенные отличия c T * есть. Например, в С требуется, чтобы T был полным (complete) типом (требование снято в С++). Также в обоих языках требуется положительность N, если оно указано. Эти требования, понятное дело, не распространяются на вариант T *.
Первый способ организации многомерного массива через индивидуальное выделение памяти для каждой строки массива — это то, как учат создавать run-time-sized двухмерные массивы студентов-первокурсников. И именно и только студенты-первокурсники так и делают. Уже ко второму курсу студент внезапно понимает, что когда речь идет об ручном выделении памяти для обычной матрицы, нет никаких причин выделять память для каждой строки индивидуально. Поэтому при "ручном" выделении памяти никто не поступает так, как показали вы, а делают несколько по-иному
double **a = new double *[n];
double *data = new double[n * n];
double *row = data;
for (int i = 0; i != n; ++i, row += n)
a[i] = row;
А теперь достаточно просто внимательно взглянуть на код выше, чтобы увидеть, что разница между этим вариантом, и вашим вариантом с пересчетом индексов заключается только в том, что вы настаиваете на постоянном пересчете индексов "на лету", то есть на явном выполнении умножения i * n при каждом доступе к элементу [i][j]. А этот вариант просто-напросто вычисляет произведение i * n и запоминает адрес a[i] для каждой строки i заранее, сохраняя его в отдельном массиве.
С концептуальной точки зрения реализацию с запоминанием адресов строк можно считать классической оптимизацией через препроцессинг: "выполним препроцессинг, потратим немного больше памяти для сохранения результатов этого препроцессинга, зато потом будем брать готовенькое, вместо того, чтобы каждый раз перевычислять заново".
Я не буду утверждать, однако, что эта овчинка стоит выделки, т.е. что такой перпроцессинг будет иметь какой-то оптимизационный эффект (скорее всего нет), но суть не в этом. Суть в том, что разница между вариантами, который вы преподнесли как "неправильный"/"неэффективный" и "правильный"/"эффективный" — эфемерна. Вы обфусцировали сущность первого варианта частоколом ненужных индивидуальных выделений памяти, и тем самым обманули сами себя.
P.S. Индивидуальное выделение памяти для строк матрицы, разумеется, не является безусловно бессмысленным. Оно может обладать ценностью в разнообразных jagged-array применениях, когда необходимо представлять "рваные"/разреженные матрицы и/или заниматься индивидуальным memory management для каждой строки. Но ваша-то статья говорит совсем не об этом…
Clang:
error: non-type template argument is not a constant expression
note: use of 'this' pointer is only allowed within the evaluation of a call to a 'constexpr' member function
Я понимаю. Однако спецификация языка однозначно утверждает, что this "shall not appear within the declaration of a static member function", даже несмотря на то, что "its type and value category is defined within a static member function as it is within a non-static member function".
Очередная GCC-шная самодеятельность (либо просто дыра). Использование this в объявлениях статических функций-членов запрещено языком. Clang корректно отлавливает эту ошибку. GCC пропускает, даже в режиме -pedantic-errors.
Вторая версия функции предназначена только для того, чтобы предотвратить ошибки/хаки типа std::forward<int &>(42), т.е. ситуации, когда пользователь случайно или намеренно указывает "нелегальную" комбинацию типа и значения. То есть вторая версия существует только ради этого static_assert внутри. Если бы мы не задавались целью следить за корректностью использования std::forward, то достатчоно было бы первой перегрузки.
возвращаемое значение будет перемещено в item без создания временного объекта
Чего??? item в данном случае является ссылкой (вы пытастесь воспользоваться мейерсовской концепцией "универсальной сслыки"). Ничего в item перемещаться, разумеется, не будет.
Никаких временных объектов не создается и в огигинальном варианте с lvalue-ссылкой.
Еще со времен С твердили: учитесь и писать и читать type-agnostic код. В не-декларативных statements языков С и С++ не должно быть никаких упоминаний конкретных типов. Именам типов место в декларациях и только в декларациях. Type-agnostic код прекрасно читаем, надо только набраться смелости и отбросить в сторону костыли, которыми являлись постоянные упоминания имен типов.
Но дурные привычки-костыли продолжают жить: молодежь тупо настаивает на явном приведении типа результата malloc в С или использовании имен типов в sizeof, ибо так якобы "надо, чтобы знать с каким типом мы работаем". И несмотря на все усилия более продвинутой мировой C/C++ community, манера тащить за собой имена типов куда надо и куда не надо умирать никак не хочет, ни в С, ни в С++.
Надеюсь, что привлекательность новых возможностей современного С++ победит дурные привычки в С++. А вот как навести порядок в рядах С-шников — не ясно.
Хоть "captureless" лямбды и приводимы к указателю на функцию, в данном случае f — отнюдь не указатель на функцию, а все таки специальный функциональный объект (closure object).
когда можно было просто присвоить к переменной a значение 10
int a = 10 — это не присваивание, а инициализауция.
Или в крайнем случае вызвать его конструктор:
У типа int нет и не может быть конструктора. Конструкторы в языке С++ бывают только у класс-типов. int — это не класс-тип.
Более того, статься совершенно не объясняет, зачем все это нужно. Пример с map::insert — мимо кассы, обо эта задача в С++ давно решается униформной инициализацией
myMap.insert({ 'a', 10 });
Причем этот вариант превосходит все предложенные варианты тем, что сразу создает пару правильного типа — std::pair<const char, int> — обратите внимание на наличие const перед char. Забывать указывать этот const — популярный огрех среди начинающих программистов. Это приводит к лишней промежуточной конверсии. К сожалению, от аналогичной проблемы же страдает и популярный вариант с make_pair. И от такой же проблемы страдает и ваш вариант.
Но, самое главное, совершенно не ясно, почему вы проигнорировали вариант
myMap.insert({ 'a', 10 });
который в данном как раз и будет выбирать способ инициализации автоматически именно по типу параметра.
Не надо пытаться непрерывно повторять одни и те же домыслы — они от этого правильнее не станут.
Если спецификация языка говорит, что поведение не определено — то поведение не определено. И в данном случае оно именно не определено. Конец дискуссии.
А то, что у какого-нибудь "васипупкина" это "всегда работало" — это не аргумент для данного разговора.
Отдельно, кстати, стоит заметить, что и на операнд унарного & тоже налагаются строгие требования
The operand of the unary & operator shall be either a function designator, the result of a [] or unary * operator, or an lvalue that designates an object that is not a bit-field and is
not declared with the register storage-class specifier.
Lvalue, полученное через ->, примененный к нулевому указателю, не "designates an object", по каковой причине такое применение унарного & вызывает неопределенное поведение.
В случае &*E для нулевого указателя E ситуацию спасает взаимная аннигиляция & и *, как уже говорилось выше. Но в случае &E->member никакой аннигиляции нет и такое применение & — нелегально.
Мысль правильная, но пример, скажем прямо, не показательный и к производительности
std::array
прямого отношения не имеющий вообще. В реальном коде подобные optimization opportunities, понятное дело, не встречаются. Если, конечно, вы их нарочно не создаете.Где и как именно в функции
begin
используются "исключения из "эквивалентности"int *
иint []
?Ну уж я не знаю, как мог Intel C++ Compiler так опростоволоситься. У меня нет под руками живого Intel C++ Compiler, но разглядывание ассемблера от Intel C++ Compiler на godbolt показывает, что для этих типов он генерирует раздельные
type_info
объекты...Стандартный язык С++ никогда не включал в себя язык С. Даже С89.
Видимо мне надо более впрямую выражать свои мыслои, а не заниматься дипломатичным хождением вокруг да около.
Поэтому еще раз: тип
int (&)[100]
не имет ничего общего с типамиint *
иint [100]
, поэтому ваш пример сf4
никакого отношения к вопросу эквивалентностиint *
иint [100]
не имеет. Т.е. написана ерунда.Далее: да, в языке С и С++, если типы
T1
ИT2
эквивалентны, то и типыT1 *
ИT2 *
тоже эквивалентны.Но вы, очевидно, запутались в природе "эквивалентностей" наблюдаемых в списках параметров функций. На самом деле в языках С и С++ типы
T [N]
иT *
эквивалентными не являются и никогда не являлись. "Эквивалентность" о которой идет речь в данном случае — не более чем следствие неявной замены типаT [N]
на типT *
в процессе интерпретации объявления функции языком. Эта подмена — четко выделенный и оговоренный в спецификациях этих языков отдельный шаг процесса такой итерпретации, а не следствие какой-то врождленной натуральной "эквивалентности"T [N]
иT *
.Еще раз напомню, кстати, что даже в параметрах функций коррекность обявления типа
T [N]
проверяется еще до выполнения вышеупомянутой подмены, то есть при неполном типеT
и/или неположительном значенииN
вы получите ошибку в языке С, в то время как проблем сT *
не было бы. Т.е никакой полной "эквивалентности" тут нет.А уж откуда вы взяли бред про
typeid(int[100]) == typeid(int[]) == typeid(int*)
— ума не приложу. В языке С++ все этиtypeid
дают попарно неравные результатыВывод
http://coliru.stacked-crooked.com/a/84c8488ed029d2e0
Я не понимаю, к чему это.
В моем комментарии нигде не сказано, что вариант с "кэшированием" результатов умножения будет каким-то образом работать быстрее. Абстрактный паттерн в данном случае действиельно "оптимизания через препреоцессинг", но работать это в данном конуретном случае будет скорее всего медленнее, о чем ясно сказано в моем комментарии. Поэтому мне не понятно, зачем вы разражаетесь в ответ исследованиями на тему скорости работы такого подхода.
Речь идет совсем не о скорости.
Целбю моего комментария является лишь демонстрация того факта, что физичекская организация хранения собственно полезных данных матрицы в обоих случаях не должна принципиально отличаться. Отличие, на которое вы напирали с самого начала статьи — лишь побочный эффект навязанного вами же способа выделения памяти. Ни более, ни менее.
Не совсем понятно, о каком "споре" идет речь, если вы фактически повторяете именно то, что я сказал во второй части своего комментария.
Замечание жа про RAII в контексте данного обсуждения неуместно — речь идет совсем не об этом.
С одной стороны, я не совсем понимаю, какое отношение вариант
f4
имеет к вопросу эквивалентности (или неэквивалентности)int []
иint *
. В этом варианте параметр имеет типint (&)[100]
, а это неint []
и неint *
. Поэтому как это может быть "одним случаем, когдаint[]
иint *
не эквивалентны" — в упор не ясно.Если говорить именно об
int []
, то никакой неэквивалентности быть не может.С другой стороны, если говорить об общем случае типа
T [N]
в списке параметров функции, то определенные отличия cT *
есть. Например, в С требуется, чтобыT
был полным (complete) типом (требование снято в С++). Также в обоих языках требуется положительностьN
, если оно указано. Эти требования, понятное дело, не распространяются на вариантT *
.Автор статьи не увидел леса за деревьями.
Первый способ организации многомерного массива через индивидуальное выделение памяти для каждой строки массива — это то, как учат создавать run-time-sized двухмерные массивы студентов-первокурсников. И именно и только студенты-первокурсники так и делают. Уже ко второму курсу студент внезапно понимает, что когда речь идет об ручном выделении памяти для обычной матрицы, нет никаких причин выделять память для каждой строки индивидуально. Поэтому при "ручном" выделении памяти никто не поступает так, как показали вы, а делают несколько по-иному
А теперь достаточно просто внимательно взглянуть на код выше, чтобы увидеть, что разница между этим вариантом, и вашим вариантом с пересчетом индексов заключается только в том, что вы настаиваете на постоянном пересчете индексов "на лету", то есть на явном выполнении умножения
i * n
при каждом доступе к элементу[i][j]
. А этот вариант просто-напросто вычисляет произведениеi * n
и запоминает адресa[i]
для каждой строкиi
заранее, сохраняя его в отдельном массиве.С концептуальной точки зрения реализацию с запоминанием адресов строк можно считать классической оптимизацией через препроцессинг: "выполним препроцессинг, потратим немного больше памяти для сохранения результатов этого препроцессинга, зато потом будем брать готовенькое, вместо того, чтобы каждый раз перевычислять заново".
Я не буду утверждать, однако, что эта овчинка стоит выделки, т.е. что такой перпроцессинг будет иметь какой-то оптимизационный эффект (скорее всего нет), но суть не в этом. Суть в том, что разница между вариантами, который вы преподнесли как "неправильный"/"неэффективный" и "правильный"/"эффективный" — эфемерна. Вы обфусцировали сущность первого варианта частоколом ненужных индивидуальных выделений памяти, и тем самым обманули сами себя.
P.S. Индивидуальное выделение памяти для строк матрицы, разумеется, не является безусловно бессмысленным. Оно может обладать ценностью в разнообразных jagged-array применениях, когда необходимо представлять "рваные"/разреженные матрицы и/или заниматься индивидуальным memory management для каждой строки. Но ваша-то статья говорит совсем не об этом…
Это в каком это компиляторе, интересно?
GCC:
error: 'this' is not a constant expression
Clang:
error: non-type template argument is not a constant expression
note: use of 'this' pointer is only allowed within the evaluation of a call to a 'constexpr' member function
Я понимаю. Однако спецификация языка однозначно утверждает, что
this
"shall not appear within the declaration of a static member function", даже несмотря на то, что "its type and value category is defined within a static member function as it is within a non-static member function".Очередная GCC-шная самодеятельность (либо просто дыра). Использование
this
в объявлениях статических функций-членов запрещено языком. Clang корректно отлавливает эту ошибку. GCC пропускает, даже в режиме-pedantic-errors
.Ответ: а почему бы и нет?
Функциональной разницы нет.
Вторая версия функции предназначена только для того, чтобы предотвратить ошибки/хаки типа
std::forward<int &>(42)
, т.е. ситуации, когда пользователь случайно или намеренно указывает "нелегальную" комбинацию типа и значения. То есть вторая версия существует только ради этогоstatic_assert
внутри. Если бы мы не задавались целью следить за корректностью использованияstd::forward
, то достатчоно было бы первой перегрузки.Чего???
item
в данном случае является ссылкой (вы пытастесь воспользоваться мейерсовской концепцией "универсальной сслыки"). Ничего вitem
перемещаться, разумеется, не будет.Никаких временных объектов не создается и в огигинальном варианте с lvalue-ссылкой.
Все прекрасно сработает именно в С++11. Синтаксис
{}
в данном случае не имеет никакого отношения кstd::initializer_list
.Еще со времен С твердили: учитесь и писать и читать type-agnostic код. В не-декларативных statements языков С и С++ не должно быть никаких упоминаний конкретных типов. Именам типов место в декларациях и только в декларациях. Type-agnostic код прекрасно читаем, надо только набраться смелости и отбросить в сторону костыли, которыми являлись постоянные упоминания имен типов.
Но дурные привычки-костыли продолжают жить: молодежь тупо настаивает на явном приведении типа результата
malloc
в С или использовании имен типов вsizeof
, ибо так якобы "надо, чтобы знать с каким типом мы работаем". И несмотря на все усилия более продвинутой мировой C/C++ community, манера тащить за собой имена типов куда надо и куда не надо умирать никак не хочет, ни в С, ни в С++.Надеюсь, что привлекательность новых возможностей современного С++ победит дурные привычки в С++. А вот как навести порядок в рядах С-шников — не ясно.
…
Также вызывает недоумение ваше
Хоть "captureless" лямбды и приводимы к указателю на функцию, в данном случае
f
— отнюдь не указатель на функцию, а все таки специальный функциональный объект (closure object).int a = 10
— это не присваивание, а инициализауция.У типа
int
нет и не может быть конструктора. Конструкторы в языке С++ бывают только у класс-типов.int
— это не класс-тип.Более того, статься совершенно не объясняет, зачем все это нужно. Пример с
map::insert
— мимо кассы, обо эта задача в С++ давно решается униформной инициализациейПричем этот вариант превосходит все предложенные варианты тем, что сразу создает пару правильного типа —
std::pair<const char, int>
— обратите внимание на наличиеconst
передchar
. Забывать указывать этотconst
— популярный огрех среди начинающих программистов. Это приводит к лишней промежуточной конверсии. К сожалению, от аналогичной проблемы же страдает и популярный вариант сmake_pair
. И от такой же проблемы страдает и ваш вариант.Но, самое главное, совершенно не ясно, почему вы проигнорировали вариант
который в данном как раз и будет выбирать способ инициализации автоматически именно по типу параметра.
Не надо пытаться непрерывно повторять одни и те же домыслы — они от этого правильнее не станут.
Если спецификация языка говорит, что поведение не определено — то поведение не определено. И в данном случае оно именно не определено. Конец дискуссии.
А то, что у какого-нибудь "васипупкина" это "всегда работало" — это не аргумент для данного разговора.
Отдельно, кстати, стоит заметить, что и на операнд унарного
&
тоже налагаются строгие требованияLvalue, полученное через
->
, примененный к нулевому указателю, не "designates an object", по каковой причине такое применение унарного&
вызывает неопределенное поведение.В случае
&*E
для нулевого указателяE
ситуацию спасает взаимная аннигиляция&
и*
, как уже говорилось выше. Но в случае&E->member
никакой аннигиляции нет и такое применение&
— нелегально.