Сложность разделения заголовка/источника класса в пространстве имен при использовании перегрузки оператора

Мое разделение класса между заголовком/источником, кажется, работает, пока я не попытаюсь работать с объявлением класса в пространстве имен. Я считаю, что это связано с областью действия оператора в указанном пространстве имен, но я не знаю синтаксиса для правильной перегрузки оператора таким образом.

NameTest.h

#pragma once

#include <iostream>

namespace testspace
{
    class Test
    {
    private:
        int number{};
    
    public:
        friend std::ostream& operator<< (std::ostream& out, const Test& test);
    };
}

NameTest.cpp

#include "NameTest.h"

using namespace testspace;

std::ostream& operator<< (std::ostream& out, const Test& test)
{
    out << test.number;
    return out;
}

Я знаю, что это разрешается так в заголовке...

(std::ostream &testspace::operator<< (std::ostream &out, const testspace::Test& test);

... и я думаю, что это как-то связано с разрешением области, но я не знаю, проблема ли это в том, как я реализую пространство имен, оператор или сам класс. По крайней мере, я знаю, что когда я удаляю объявление класса из пространства имен, все работает нормально. Это связано с разрешением области в .cpp? И как бы я это реализовал?

Заранее спасибо.

Обновлено: test.number недоступен в определении перегрузки оператора в его нынешнем виде...

using namespace X; просто добавляет пространство имен для целей поиска. Чтобы определить что-то в пространстве имен, вам нужно namespace X { ... }
john 06.08.2023 20:31
namespace testspace { std::ostream& operator<< (std::ostream& out, const Test& test) { ... } } в исходном файле - так же, как в заголовочном файле.
Jesper Juhl 06.08.2023 20:33

@heap underrun - так что у меня возникнут проблемы с 'namespace testspace {' как в заголовке, так и в источнике? Или он просто добавляет то, что существует в пространстве имен?

rfrankmcm 06.08.2023 20:34

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

Jesper Juhl 06.08.2023 20:35

Это помогает Джесперу, спасибо!

rfrankmcm 06.08.2023 20:36
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Когда вы говорите using namespace testspace; в исходном файле, это просто импортирует имена из пространства имен для поиска имен. Если вы хотите добавить что-то в пространство имен, например, ваш operator<<, вам нужно открыть пространство имен с помощью

namespace testspace {

а затем добавьте реализацию оператора, а затем снова закройте пространство имен

}

Вы можете открывать пространство имен для дополнений столько раз, сколько вам нужно/хотите, в нескольких файлах или внутри одного и того же файла. Это все добавки.

Особый статус операторских функций

Есть тонкая причина, по которой определение operator<< должно быть сделано внутри пространства имен testspace. Это связано с особым статусом, который присваивается operator функциям, объявленным в классе. Обычно на члены класса нужно ссылаться с помощью dot operator. Однако мы обычно не делаем этого с операторами. Вместо этого мы просто используем их.

Это работает, потому что операторы, объявленные внутри класса, автоматически становятся видимыми в области, в которой объявлен класс. Это место, куда смотрит компоновщик, когда ему нужно найти определение для вашего оператора-друга.

Вы сказали компоновщику, что ваш оператор определен в пространстве имен testspace, но оператор, который вы определяете в файле NameTest.cpp, находится на «уровне файла».

Исправление предоставлено @Jesper Juhl. Переместите определение в пространство имен testspace.

// NameTest.cpp
#include "NameTest.h"
using namespace testspace;
namespace testspace
{
    std::ostream& operator<< (std::ostream& out, const Test& test)
    {
        out << test.number;
        return out;
    }
}

Доказательство точки

Вот небольшое излишество, которое доказывает эту точку. Это то, что вы бы сделали, если бы решили сохранить определение operater<< на «уровне файлов».

// NameTest.h
#pragma once
#include <iostream>

namespace testspace
{
    // Forward declaration tells everyone that class Test 
    // resides in namespace testspace.
    class Test;
}
// The operator declared here is declared at the "file-level".
// Its right operand, however, will be found in namespace testspace.
std::ostream& operator<< (std::ostream& out, const testspace::Test& test);

namespace testspace
{
    class Test
    {
    private:
        int number{};

    public:
        // The friend declared here is an operator that has "file-level" scope. 
        // Note the :: right before the keyword operator.
        friend std::ostream& ::operator<< (std::ostream& out, const Test& test);
    };
}
// NameTest.cpp
#include "NameTest.h"
using namespace testspace;

// This operator is defined at the "file level."
// The right operand must be scoped to namespace testspace.
std::ostream& operator<< (std::ostream& out, const testspace::Test& test)
{
    out << test.number;
    return out;
}

Хотите верьте, хотите нет, но эта программа работает, как и другие, приведенные в этом ответе. Я протестировал их со следующим драйвером, который выводит t : 0.

// main.cpp
#include <iostream>
#include "NameTest.h"
int main()
{
    testspace::Test t;
    std::cout << "t : " << t << "\n\n";
    return 0;
}

Альтернатива скрытым друзьям

Однако в целом мне не нравится ни один из приведенных выше ответов. Вместо этого я бы использовал идиому «скрытые друзья». Этот метод просто перемещает определение operator<< в сам класс.

Вы можете узнать больше о скрытых друзьях в моем ответе на этот вопрос StackOverflow.

Я часто inline оператор, но это не обязательно.

// NameTest.h
#pragma once
#include <iostream>

namespace testspace
{
    class Test
    {
    private:
        int number{};

    private:
        inline friend auto operator<< (std::ostream& out, const Test& test)
            -> std::ostream&
        {
            out << test.number;
            return out;
        }
    };
}

Во избежание путаницы я обычно объявляю своих скрытых друзей как public. Однако в этом примере я сделал это private. На самом деле access specifier может быть public, private или protected. Неважно какой. Дружба не имеет ничего общего со спецификаторами доступа.

Определяемый скрытый друг не является членом класса. Это оператор, видимый в области видимости за пределами класса и фактически находящийся там. Для функции, которая не является членом класса и находится вне класса, нет смысла говорить о public, private и protected.

Кстати, если вы используете идиому скрытого друга, не забудьте удалить определение operator<< из файла NameTest.cpp.

Боковые примечания:

  1. Если вам нужно пространство имен с длинным именем, рассмотрите возможность использования «псевдонима пространства имен». Вот, например, я бы, наверное, создал псевдоним ts для testspace.
namespace ts = testspace;
  1. В большинстве случаев вы не хотите использовать «директивы использования», такие как using namespace testspace и using namespace std. Просто запишите нужные имена с полной областью действия. Например, учитывая псевдоним пространства имен ts, легко написать ts::Test и так далее.

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