GCC14 выполняет несколько неявных преобразований вместо одного соответствующего явного преобразования

#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

Этот интересный gcc 14 ведет себя иначе, чем 13: godbolt.org/z/nn8EsEPP9

Marek R 08.08.2024 17:28

Почему бы просто не удалить explicit из explicit operator std::string() &&;? Демо

user12002570 08.08.2024 17:42

Тогда существующий код полон неоднозначных вызовов функций, которые имеют перегрузки как для c-str, так и для std::string.

Martin Horský 08.08.2024 17:44
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
3
85
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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)). Это не будет прямой инициализацией, не так ли?

Martin Horský 08.08.2024 17:54

@MartinHorský Нет, static_cast в конце концов также выполняет прямую инициализацию. Это имеет тот же эффект. Как я исправил в своем ответе, я не думаю, что в настоящее время существует какой-либо способ выбрать функцию преобразования, кроме как путем прямого вызова ее.

user17732522 08.08.2024 17:55

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