#include <cstdio>
#include <string>
class A {
std::string data;
public:
A() = default;
explicit A (const char* data) : data(data) {}
operator const char* () const;
explicit operator std::string() &&;
};
A::operator const char*() const {
printf("A -> const char*\n");
return data.c_str();
}
A::operator std::string() && {
printf("A -> std::string\n");
return data;
}
int main() {
A a("lorem");
std::string s(std::move(a));
printf("%s\n", s.c_str());
return 0;
}
Приведенный выше код печатает «A -> std::string» в gcc13 и «A -> const char*» в gcc14. Clang (18.1.8) делает то же самое, что и gcc13. Все компиляторы вызывались с помощью -Wall -pedantic --std=c++17
Код представляет собой случай минимального воспроизведения.
Я пытаюсь добавить std::string поддержку пользовательского класса A в очень старом проекте. К сожалению, A должен иметь неявное преобразование в c-str, чтобы не нарушить существующий код.
У меня была рабочая версия, но на gcc14 она не работает.
Я хочу, чтобы оператор преобразования перемещения вызывался, когда сайт вызова пытается создать std::string из ссылки rvalue A. Все c-cast, function-cast, static_cast и инициализация работали на gcc13.
Я думаю, что происходит то, что в gcc13 преобразование перемещения вызывается, как и ожидалось, и происходит только перемещение. В gcc14 выполняется двухэтапное неявное преобразование с использованием неявных A::operator const char*() и std::string(const char*). Это создает вторую строку, используя копию вместо перемещения.
Имя оператора преобразования, например C c = std::move(a).operator std::string();, вызывает желаемое преобразование.
Есть ли способ убедить gcc14 в желаемом поведении?
Обновлено:
В моем случае оператор std::string должен быть явным. Существующий код использует функции стандартной библиотеки, которые перегружены как для c-str, так и для std::string. Удаление явного сделает эти вызовы неоднозначными.
Я также попытался сделать более общий случай воспроизведения. Это происходит, даже если все задействованные типы являются пользовательскими.
#include <cstdio>
class B {
};
class C;
class A {
B data;
public:
A() = default;
explicit A (B data) : data(data) {}
operator B () const;
explicit operator C() const;
};
class C {
B data;
public:
C(B data) : data(data) {
printf("C from B\n");
}
};
A::operator B() const {
printf("A -> B\n");
return data;
}
A::operator C() const {
printf("A -> C\n");
return C(data);
}
int main() {
A a(B{});
C s = static_cast<C>(a);
return 0;
}
С помощью gcc13 и clang это печатает
A -> C
C from B
и с gcc14 он печатает
A -> B
C from B
Почему бы просто не удалить explicit из explicit operator std::string() &&;? Демо
Тогда существующий код полон неоднозначных вызовов функций, которые имеют перегрузки как для c-str, так и для std::string.





GCC 14 здесь кажется правильным.
Есть ли способ убедить gcc14 в желаемом поведении?
Да, в качестве обходного пути вы можете сделать оператор преобразования неявным.
class A {
//other code as before
//vv----------------------------->removed explicit from here
operator std::string() &&;
};
GCC 14 формально верен (а другие компиляторы (версии) — нет):
std::string s(std::move(a)); — это прямая инициализация, которая выполняет разрешение перегрузки для всех конструкторов std::string и только тех, у которых есть список аргументов (std::move(a)).
В этом разрешении перегрузки конструктор std::string, который принимает const char*, является единственным жизнеспособным. Вы ожидаете, что будет использоваться конструктор перемещения с типом аргумента std::string&&, но он нежизнеспособен. При преобразовании аргументов функций нельзя использовать explicit функции преобразования.
Тем не менее, существует открытый вопрос CWG 2327, связанный с этим. Удивительно, что функции преобразования не могут использоваться непосредственно для прямой инициализации, а также не может быть никакого исключения копирования для промежуточного временного объекта, инициализированного из вызова функции преобразования, когда выбран конструктор перемещения.
Однако документ P2828R2, в котором содержится текущее предложение по устранению проблемы CWG, также не заставит ваш конкретный случай работать так, как вы намереваетесь.
Я считаю, что проблему необходимо решить, чтобы операторы преобразования рассматривались вместе с конструкторами, как это предложено в CWG 2327. В противном случае это приводит к тому, что даже static_cast, который должен выполнять явные преобразования, но в конечном итоге все равно использует прямую инициализацию, чтобы не работайте с конструкторами explicit для типов классов. Поведение остальных компиляторов здесь более разумно.
Чтобы выполнить конвертацию, которую вы хотите, вам необходимо напрямую вызвать оператору:
auto s = std::move(a).operator std::string();
Насколько я могу судить, другого способа выбрать эту функцию преобразования для инициализации std::string не существует, если вы не измените определение класса.
Это происходит, даже если значение инициализируется, а затем присваивается с помощью static_cast<std::string>(std::move(a)). Это не будет прямой инициализацией, не так ли?
@MartinHorský Нет, static_cast в конце концов также выполняет прямую инициализацию. Это имеет тот же эффект. Как я исправил в своем ответе, я не думаю, что в настоящее время существует какой-либо способ выбрать функцию преобразования, кроме как путем прямого вызова ее.
Этот интересный gcc 14 ведет себя иначе, чем 13: godbolt.org/z/nn8EsEPP9