Определите функцию `static constexpr` в исходном файле

У меня есть class в заголовочном файле, члены которого определены внутри класса pimpl. Идея состоит в том, что я использую этот метод (в основном std::aligned_storage_t и указатель, но размер и выравнивание класса должны быть указаны при объявлении объекта) для размещения класса pimpl в стеке. Я хочу сделать код кросс-компилятором, чтобы угадывать не вариант, поэтому я определил 2 privatestatic constexpr функции: impl_size и impl_align, которые определены в соответствующем исходном файле и в основном возвращают sizeof(pimpl) и alignof(pimpl). Проблема в том, что я получаю следующую ошибку от MSVC (не проверено на других компиляторах):

expression must have a constant value -- constexpr function function "Type::impl_size" (declared at line 70) is not defined

строка 70 — это место, где impl_size определено в заголовке.

MCVE:

#include <cstddef>

template <typename T1, std::size_t N1, std::size_t N2>
struct Pimpl;

class Type
{
  private:
    static constexpr std::size_t impl_size() noexcept; // line 70
    static constexpr std::size_t impl_align() noexcept;

    struct impl {};

    Pimpl<impl, impl_size(), impl_align()> data; // error happens here
};

constexpr std::size_t Type::impl_size() noexcept
{
    return sizeof(Type::impl);
}

constexpr std::size_t Type::impl_align() noexcept
{
    return alignof(Type::impl);
}

Пожалуйста, отредактируйте свой пост и создайте минимальный воспроизводимый пример. Мы не можем исправить код, который не видим.

alter_igel 13.12.2020 21:18

Я добавил минималистичный пример. Если этого недостаточно, я рад «расширить» его до более полного.

Terens Tare 13.12.2020 21:41

Понятно, что компилятору необходимо знать значение impl_size() во время компиляции, а для этого ему нужно видеть свое тело везде, где header.hpp включен.

Igor Tandetnik 13.12.2020 21:46

@TerensTare MCVE всегда должен быть завершен. Без исключений. Делайте это с самого начала, чтобы мы все не тратили время на то, чтобы снова и снова спрашивать об этом, как мы делаем по любому другому вопросу, который мы получаем ежедневно.

Asteroids With Wings 13.12.2020 21:48
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
500
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это то, что он говорит: вы не определили свою функцию-член, когда объявили ее constexpr.

Вы должны предоставить определение немедленно, встроенное в определение класса.

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

Есть пара проблем с вашим кодом, которые кажутся мне странными. Суть идиомы "PImpl" состоит в том, чтобы отделить интерфейс от его реализации, как правило, для упрощения компиляции при изменении реализации. Однако, определяя свой struct impl внутри своего класса интерфейса и используя его в шаблоне в том же классе, вы, по сути, заставляете реализацию быть связанной с интерфейсом.

sizeof и alignof требуют полного типа, которого impl не имеет (EDIT на момент написания, impl был просто объявлен вперед), поэтому даже после исправления проблем с Type::impl_size() и Type::impl_align() вы столкнетесь с этой проблемой.

Немедленным и несколько поверхностным решением вашей проблемы было бы сделать impl полным типом (и определить его в точке объявления), а также превратить impl_size() и impl_align() в функции inline static constexpr, которые также определены на месте:

class type
{
  // ...

  private:
    struct impl {
        // struct definition, so that impl is a complete type
    };

    inline static constexpr impl_size() noexcept {
        return sizeof(impl);
    }
    inline static constexpr impl_align() noexcept {
        return alignof(impl);
    }
    
    Pimpl<impl, impl_size(), impl_align()> data;
};

Это все еще немного пахнет, потому что impl_size и impl_align — это бессмысленный шаблон, а ваш шаблон PImpl может иметь всю ту же информацию непосредственно из своего первого параметра:

template<typename T>
class Pimpl {
    static constexpr std::size_t Size = sizeof(T);
    static constexpr std::size_t Alignment = alignof(T);
};

class type {
  private:
    struct impl {};
    
    Pimpl<impl> data;
};

Это, конечно, также требует, чтобы impl был полным типом. И это все равно потребуется, если struct impl является вложенным классом.

Похоже, вы пытаетесь сделать здесь какое-то стирание типов (или есть другая веская причина, по которой вам нужен размер и выравнивание impl?), но непреднамеренно ввели много зависимостей от реализации и задействованных типов.

Я бы предложил следующее: предварительно объявите свой класс impl в области пространства имен и просто используйте std::unique_ptr<impl> для своей реализации. Затем impl можно определить в файле реализации. Обратите внимание, что std::unique_ptr не требует полного типа.

#include <iostream>
#include <memory>

// header.hpp
struct impl;

class type {
public:
    type();
    void doThing();
    
private:
    std::unique_ptr<impl> m_pImpl;
};

// source.cpp
struct impl {
    void doThingImpl() {
        std::cout << "Did a thing";
    }
};

type::type()
    : m_pImpl(std::make_unique<impl>()) {
      
}

void type::doThing() {
    m_pImpl->doThingImpl();
}

int main(){
    auto t = type{};
    t.doThing();
    
    return 0;
}

Живая демонстрация

Спасибо за Ваш ответ. Я пытаюсь избежать распределения кучи (это "забавный" проект), но, думаю, я буду использовать умные указатели.

Terens Tare 13.12.2020 22:00

Вы можете разместить новый объект impl внутри блока хранилища, не распределенного в куче, непосредственно в классе type, но вам потребуется жестко закодировать размер и выравнивание (или их верхние границы) объекта impl для это работает (потому что вы не можете получить размер или выравнивание неполного типа). Это может быть забавно, если вам нужна экстремальная производительность, но я бы не стал поощрять это из соображений безопасности памяти и удобства обслуживания.

alter_igel 13.12.2020 22:03

Это была идея, использованная с std::aligned_storage_t, но я думаю, что тогда лучше использовать что-то еще, чем std::aligned_storage_t. еще раз спасибо

Terens Tare 13.12.2020 22:09

На самом деле, если бы выделение кучи было моей самой большой проблемой, я бы просто реализовал все type, используя обычные функции-члены, даже если это означает немного более длительную компиляцию здесь или там.

alter_igel 13.12.2020 22:12

Меня не волнует время компиляции, я просто хочу не показывать членов Type в заголовке. Это потому, что я хочу создать source.cpp как DLL, и я получаю предупреждения о создании экземпляров шаблонов от MSVC (один из элементов — это std::map), поэтому я подумал, что, может быть, было бы лучше, если бы я просто «скрыл» эти элементы.

Terens Tare 13.12.2020 22:16

@TerensTare Мне стало любопытно, и я думаю, что у меня есть рабочая демонстрация относительно безопасного размещения нового PImpl: coliru.stacked-crooked.com/a/9237399182be0d1f Размер и выравнивание проверяются во время компиляции. Если проверки не пройдены, вы можете запросить их, просто определив макрос GIVE_ME_THE_SIZE и используя выходные данные для исправления aligned_storage_t

alter_igel 14.12.2020 00:13

Еще раз спасибо @alter igel. Проблема в том, что я хочу, чтобы это был кросс-компилятор, но, поскольку стандарт не накладывает никаких ограничений на размер std:: типов, я не могу просто закодировать их. Я предполагаю, что макросы, специфичные для компилятора, будут работать.

Terens Tare 14.12.2020 09:44

Верхние границы тоже должны работать, например impl_size => max_impl_size и static_assert(sizeof(impl) <= max_impl_size);

alter_igel 14.12.2020 18:40

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