Почему std :: any_cast не поддерживает неявное преобразование?

Почему std::any_cast выдает исключение std::bad_any_cast, если возможно неявное преобразование фактического сохраненного типа в запрошенный тип?

Например:

std::any a = 10;  // holds an int now
auto b = std::any_cast<long>(a);   // throws bad_any_cast exception

Почему это запрещено, и есть ли обходной путь, позволяющий неявное преобразование (в случае, если точный тип, который содержит std::any, неизвестен)?

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

Konrad Rudolph 22.03.2018 18:05
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
38
1
5 358
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

std::any_cast указан в терминах typeid. Процитирую cppreference по этому поводу:

Throws std::bad_any_cast if the typeid of the requested ValueType does not match that of the contents of operand.

Поскольку typeid не позволяет реализации «выяснить», возможно ли неявное преобразование, то (насколько мне известно) any_cast также не может знать, что это возможно.

Другими словами, стирание типов, предоставляемое std::any, полагается на информацию, доступную только во время выполнения. И эта информация не так богата, как информация, имеющаяся у компилятора для вычисления преобразований. Это стоимость стирания типа в C++ 17.

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

Timo 22.03.2018 13:02

@Timo - Разве это невозможно? Если у вас есть идея, как это сделать, это будет большой прорыв. Я не знаю обходного пути. Чтобы даже попытаться выполнить преобразование, вам необходимо получить ссылку правильного типа на удаленный объект. Но для его получения нужно заранее знать тип (статически). Это проблема курицы и яйца, чем больше об этом думаешь. В настоящее время C++ не поддерживает ничего подобного.

StoryTeller - Unslander Monica 22.03.2018 13:05

Конечно, можно создать std::any с более широким контрактом. Например, любое преобразование в основание или перекрестное приведение можно обрабатывать на основе средств dynamic_cast, а любое примитивное преобразование можно жестко запрограммировать. В то время как это желательно, это совсем другое дело, однако, прежде всего потому, что это было бы не так дешево. как просто проверить typeid.

Matthieu M. 22.03.2018 15:48

@MFH: Я понятия не имею, что привело к этому наблюдению ... Если вы не можете представить, как использовать здесь dynamic_cast, я приглашаю вас узнать о методах стирания шрифтов. Например, как shared_ptr<void> знает, как удалить базовое значение? Если вы можете ответить на этот вопрос, тогда вы сможете понять, как использовать dynamic_cast внутри реализации any.

Matthieu M. 23.03.2018 09:00

@MatthieuM. реализация, о которой я могу думать, потребовала бы шаблонной виртуальной функции, что невозможно.

Simple 23.03.2018 13:39

@Simple: В моем последнем комментарии есть короткий путь, который может вас сбить с толку, извините; в этом первом комментарии я упомянул удобства. То есть механизм, лежащий в основе реализации dynamic_cast, имеет возможность переходить от указателя источника к указателю назначения без использования шаблонного кода. См. github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/… для примера реализации.

Matthieu M. 23.03.2018 14:47

@MatthieuM. Вы действительно проверили код, который вы связали? Потому что он обязательно потерпит неудачу, если ваш void * не относится к полиморфному типу! Поэтому, если вы не хотите принудительно накладывать дополнительные накладные расходы (например, std :: function) на каждого пользователя std :: any, это невозможно сделать!

MFH 27.03.2018 15:19

@MFH: Да, dynamic_cast всегда работает только с полиморфными типами. Когда хранение значение, вы статически знаете, является ли оно полиморфным или нет, примитивным оно или нет и т. д., Поэтому на этом этапе вы можете применить методы стирания типов для кодирования соответствующих знаний в v-таблице (вместо хранение typeid), то есть реализовать соответствующую стратегию. Для примитива это позволит перекрестное приведение к фиксированному списку других примитивов. Для полиморфного значения он может использовать информацию RTTI, как это делает dynamic_cast. Простой, работоспособный, небольшие накладные расходы по сравнению с текущим подходом.

Matthieu M. 27.03.2018 18:28

@MatthieuM. Итак, как я уже сказал: вы заставляете всех платить за вызов vtable, что противоречит основным принципам C++ и особенно не подходит для типа "словарный запас". Кроме того: ваше перекрестное приведение не будет работать для any_casts на примитивные ссылки ...

MFH 28.03.2018 13:07

@MFH: Нет. У вас может быть два метода: один для точных типов и для перекрестных типов. Точный тип просто сравнивает виртуальные указатели, что так же дорого, как сравнение typeid. Что касается примитивов, я уже упоминал, что вам потребуются другие стратегии.

Matthieu M. 28.03.2018 13:21

@MatthieuM. У вас не должно быть двух способов извлечения данных, поскольку any_cast предназначен для имитации преобразования C++. В любом случае ты прав - я упустил из виду требование для глубоких копий, которое делает vtables уже необходимыми для std :: any, так что да, добавление более широкой поддержки кастинга было бы возможно (хотя проблема висящих ссылок была бы усилена этой функцией)

MFH 28.03.2018 14:35

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

Есть языки, которые могут это сделать; каждый двоичный файл поставляется с байт-кодом IR (или исходным кодом) и интерпретатором / компилятором. Эти языки, как правило, в 2 раза или более медленнее, чем C++, в большинстве задач и имеют значительно больший объем памяти. Возможно, эти функции можно получить без такой стоимости, но ни у кого нет такого языка, о котором я знаю.

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

Языки, которые делают это, обычно используют JIT-компиляцию для достижения скорости, сопоставимой с C++. Хорошая вещь в C++ (и ржавчине и т. д.) Заключается в том, что они предсказуемы и детерминированы (для машины, а не для разработчика - если у вас нет неопределенного поведения) и строгие - отсутствие GC означает, что у вас есть строго больший контроль над распределением для пример.

Benjamin Gruenbaum 23.03.2018 14:01

@BenjaminGruenbaum Я согласен: коэффициент 2 "сопоставим". Возможно, удастся написать язык, который выполняет все вышеперечисленное и не имеет ~ 2-кратного замедления, но я еще не встречал этого. И я понимаю, что JIT может обрабатывать некоторые микробенчмарки и в некоторых случаях соответствовать скорости C++. Возможно, именно почти единообразное использование GC на языках JIT является настоящим замедлением, и язык JIT без GC мог бы обрабатывать полное отражение / повторение кодовой базы без снижения производительности. Пожалуйста, укажите мне на этот язык, я бы с удовольствием посмотрел на него.

Yakk - Adam Nevraumont 23.03.2018 14:05

std::anyимеет должен быть реализован со стиранием типа. Это потому, что он может хранить тип любой и не может быть шаблоном. На данный момент в C++ нет других функций для достижения этой цели.

Это означает, что std::any сохранит указатель со стиранием типа, void* и std::any_cast преобразуют этот указатель в указанный тип и все. Он просто выполняет проверку работоспособности с помощью typeid, прежде чем проверить, является ли тип, к которому вы его применяете, тем, который хранится в any.

При текущей реализации невозможно разрешить неявные преобразования. Подумайте об этом (пока игнорируйте проверку typeid).

std::any_cast<long>(a);

a хранит int, а не long. Как std::any должен это знать? Он может просто привести свой void* к указанному типу, разыменовать его и вернуть. Приведение указателя от одного типа к другому является строгим нарушением псевдонима и приводит к UB, так что это плохая идея.

std::any должен был бы сохранить фактический тип объекта, хранящегося в нем, что невозможно. Сейчас вы не можете хранить типы в C++. Он может поддерживать список типов вместе с соответствующими typeid и переключать их, чтобы получить текущий тип и выполнить неявное преобразование. Но нет способа сделать это для одиночного типа каждый, который вы собираетесь использовать. Типы, определяемые пользователем, в любом случае не будут работать, и вам придется полагаться на такие вещи, как макросы, чтобы «зарегистрировать» свой тип и сгенерировать соответствующий случай переключения для него 1.

Может быть, примерно так:

template<typename T>
T any_cast(const any &Any) {
  const auto Typeid = Any.typeid();
  if (Typeid == typeid(int))
    return *static_cast<int *>(Any.ptr());
  else if (Typeid == typeid(long))
    return *static_cast<long *>(Any.ptr());
  // and so on. Add your macro magic here.

  // What should happen if a type is not registered?
}

Это хорошее решение? Нет, конечно. Переключение стоит дорого, и мантра C++ - «вы не платите за то, что не используете», так что нет, в настоящее время нет способа добиться этого. Подход также «хакерский» и очень хрупкий (что произойдет, если вы забудете зарегистрировать тип). Короче говоря, возможные выгоды от выполнения чего-то подобного вообще не стоят проблем.

is there a workaround to allow an implicit conversion (in case the exact type that std::any holds is unknown)?

Да, реализовать std::any (или аналогичный тип) и std::any_cast самостоятельно, используя упомянутый выше подход макросов 1. Я не буду рекомендовать это. Если вы не знаете и не можете знать, какой тип хранится в std::any и вам нужно получить к нему доступ, у вас возможный недостаток в конструкции.


1: На самом деле не знаю, возможно ли это, я не так хорош в использовании макросов (ab). Вы также можете жестко запрограммировать свои типы для своей пользовательской реализации или использовать для этого отдельный инструмент.

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

Cort Ammon 22.03.2018 23:45

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

std::any("blabla");

будет работать, но будет хранить char const*, а не массив. Вы можете добавить такую ​​функцию в свой собственный настроенный any, но тогда вам нужно будет сохранить указатель на строковый литерал, выполнив:

any(&*"blabla");

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

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

Этот вопрос поставлен неправильно; неявное преобразование в правильный тип в принципе возможно, но отключено. Это ограничение, вероятно, существует для поддержания определенного уровня безопасности или имитации явного приведения, необходимого (через указатель) в C-версии any (void*). (Пример реализации ниже показывает, что это возможно.)

При этом ваш целевой код по-прежнему не работает, потому что вам нужно знать точный тип перед преобразованием, но это может сработать в принципе:

any a = 10;  // holds an int now
long b = int(a); // possible but today's it should be: long b = any_cast<int>(a);

Чтобы показать, что неявные преобразования технически возможны (но могут не работать во время выполнения):

#include<boost/any.hpp>

struct myany : boost::any{
    using boost::any::any;
    template<class T> operator T() const{return boost::any_cast<T>(*this);}
};

int main(){

    boost::any ba = 10;
//  int bai = ba; // error, no implicit conversion

    myany ma = 10; // literal 10 is an int
    int mai = ma; // implicit conversion is possible, other target types will fail (with an exception)
    assert(mai == 10);

    ma = std::string{"hello"};
    std::string mas = ma;
    assert( mas == "hello" );
 }

Другие вопросы по теме