Почему 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, неизвестен)?





std::any_cast указан в терминах typeid. Процитирую cppreference по этому поводу:
Throws
std::bad_any_castif thetypeidof the requestedValueTypedoes not match that of the contents of operand.
Поскольку typeid не позволяет реализации «выяснить», возможно ли неявное преобразование, то (насколько мне известно) any_cast также не может знать, что это возможно.
Другими словами, стирание типов, предоставляемое std::any, полагается на информацию, доступную только во время выполнения. И эта информация не так богата, как информация, имеющаяся у компилятора для вычисления преобразований. Это стоимость стирания типа в C++ 17.
Хорошо, это имеет смысл, если он связан с typeid, и если я понимаю ваше редактирование, невозможно сделать обходной путь, не зная точного типа?
@Timo - Разве это невозможно? Если у вас есть идея, как это сделать, это будет большой прорыв. Я не знаю обходного пути. Чтобы даже попытаться выполнить преобразование, вам необходимо получить ссылку правильного типа на удаленный объект. Но для его получения нужно заранее знать тип (статически). Это проблема курицы и яйца, чем больше об этом думаешь. В настоящее время C++ не поддерживает ничего подобного.
Конечно, можно создать std::any с более широким контрактом. Например, любое преобразование в основание или перекрестное приведение можно обрабатывать на основе средств dynamic_cast, а любое примитивное преобразование можно жестко запрограммировать. В то время как это желательно, это совсем другое дело, однако, прежде всего потому, что это было бы не так дешево. как просто проверить typeid.
@MFH: Я понятия не имею, что привело к этому наблюдению ... Если вы не можете представить, как использовать здесь dynamic_cast, я приглашаю вас узнать о методах стирания шрифтов. Например, как shared_ptr<void> знает, как удалить базовое значение? Если вы можете ответить на этот вопрос, тогда вы сможете понять, как использовать dynamic_cast внутри реализации any.
@MatthieuM. реализация, о которой я могу думать, потребовала бы шаблонной виртуальной функции, что невозможно.
@Simple: В моем последнем комментарии есть короткий путь, который может вас сбить с толку, извините; в этом первом комментарии я упомянул удобства. То есть механизм, лежащий в основе реализации dynamic_cast, имеет возможность переходить от указателя источника к указателю назначения без использования шаблонного кода. См. github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/… для примера реализации.
@MatthieuM. Вы действительно проверили код, который вы связали? Потому что он обязательно потерпит неудачу, если ваш void * не относится к полиморфному типу! Поэтому, если вы не хотите принудительно накладывать дополнительные накладные расходы (например, std :: function) на каждого пользователя std :: any, это невозможно сделать!
@MFH: Да, dynamic_cast всегда работает только с полиморфными типами. Когда хранение значение, вы статически знаете, является ли оно полиморфным или нет, примитивным оно или нет и т. д., Поэтому на этом этапе вы можете применить методы стирания типов для кодирования соответствующих знаний в v-таблице (вместо хранение typeid), то есть реализовать соответствующую стратегию. Для примитива это позволит перекрестное приведение к фиксированному списку других примитивов. Для полиморфного значения он может использовать информацию RTTI, как это делает dynamic_cast. Простой, работоспособный, небольшие накладные расходы по сравнению с текущим подходом.
@MatthieuM. Итак, как я уже сказал: вы заставляете всех платить за вызов vtable, что противоречит основным принципам C++ и особенно не подходит для типа "словарный запас". Кроме того: ваше перекрестное приведение не будет работать для any_casts на примитивные ссылки ...
@MFH: Нет. У вас может быть два метода: один для точных типов и для перекрестных типов. Точный тип просто сравнивает виртуальные указатели, что так же дорого, как сравнение typeid. Что касается примитивов, я уже упоминал, что вам потребуются другие стратегии.
@MatthieuM. У вас не должно быть двух способов извлечения данных, поскольку any_cast предназначен для имитации преобразования C++. В любом случае ты прав - я упустил из виду требование для глубоких копий, которое делает vtables уже необходимыми для std :: any, так что да, добавление более широкой поддержки кастинга было бы возможно (хотя проблема висящих ссылок была бы усилена этой функцией)
Чтобы делать то, что вы хотите, вам потребуется полное отражение и реификация кода. Это означает, что каждая деталь каждого типа должна быть сохранена в каждом двоичном файле (и каждая сигнатура каждой функции для каждого типа! И каждый шаблон где угодно!), И когда вы просите преобразовать из любого в тип X, вы должны передать данные о X в любой, которые будут содержать достаточно информации о типе, который он содержит, чтобы в основном попытаться скомпилировать преобразование в X и потерпеть неудачу или нет.
Есть языки, которые могут это сделать; каждый двоичный файл поставляется с байт-кодом IR (или исходным кодом) и интерпретатором / компилятором. Эти языки, как правило, в 2 раза или более медленнее, чем C++, в большинстве задач и имеют значительно больший объем памяти. Возможно, эти функции можно получить без такой стоимости, но ни у кого нет такого языка, о котором я знаю.
C++ не имеет такой возможности. Вместо этого он забывает почти все факты о типах во время компиляции. Для любого он запоминает typeid, который можно использовать для получения точного совпадения, и то, как преобразовать его хранилище в указанное точное совпадение.
Языки, которые делают это, обычно используют JIT-компиляцию для достижения скорости, сопоставимой с C++. Хорошая вещь в C++ (и ржавчине и т. д.) Заключается в том, что они предсказуемы и детерминированы (для машины, а не для разработчика - если у вас нет неопределенного поведения) и строгие - отсутствие GC означает, что у вас есть строго больший контроль над распределением для пример.
@BenjaminGruenbaum Я согласен: коэффициент 2 "сопоставим". Возможно, удастся написать язык, который выполняет все вышеперечисленное и не имеет ~ 2-кратного замедления, но я еще не встречал этого. И я понимаю, что JIT может обрабатывать некоторые микробенчмарки и в некоторых случаях соответствовать скорости C++. Возможно, именно почти единообразное использование GC на языках JIT является настоящим замедлением, и язык JIT без GC мог бы обрабатывать полное отражение / повторение кодовой базы без снижения производительности. Пожалуйста, укажите мне на этот язык, я бы с удовольствием посмотрел на него.
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). Вы также можете жестко запрограммировать свои типы для своей пользовательской реализации или использовать для этого отдельный инструмент.
Такого рода фокусы с кастингом с регистрацией, безусловно, возможны. Я сделал это. Но результирующий код определенно является типом кода нет, который вы ожидаете найти в стандартной языковой функции. Это больше похоже на специальную зависящую от предметной области функцию (именно так было, когда я ее реализовал)
Это можно реализовать, попробовав неявное преобразование на случай непредвиденных обстоятельств, если идентификатор запрошенного типа не совпадает с идентификатором сохраненного типа. Но это потребует затрат и, следовательно, нарушит принцип "не платите за то, что не используете". Еще один недостаток 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" );
}
Это невозможно ни необходимо! Фактически, другие языки имеют такое же ограничение на распаковку. Я не могу придумать ни одного сценария, в котором это было бы проблематично.