Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add overloads to show additional custom message on failure #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 26, 2021

Conversation

hobovsky
Copy link

@hobovsky hobovsky commented Apr 21, 2021

Another iteration on idea from #1 (comment) .

Proposed API:

Assert::That(
    shortestDistance(1, 2, 3), 
    EqualsWithDelta(4.242640687, 1e-9), 
    ExtraMessage("Invalid result for input: x=1, y=2, z=3")
);

MessageSupplierType type parameter of Assert::That would allow for different implementations of message suppliers. MessageStringSupplier is provided by default and can be constructed with ExtraMessage convenience function, but it would be possible to create other suppliers too, for example utilising string streams or {fmt}.

@hobovsky
Copy link
Author

Added a proposal of a message supplier based on string streams.

Example usage:

Assert::That(
    shortestDistance(1, 2, 3), 
    EqualsWithDelta(4.242640687, 1e-9), 
    MessageBuilder("Invalid result for input: x=") << 1 << ", y=" << 2 << ", z=" << 3
);

@hobovsky
Copy link
Author

hobovsky commented Apr 22, 2021

Added remaining overloads. Now every original overload of That has its corresponding overload accepting a message supplier (except That(bool), see below).

Obstacles:

Default attributes file_name = "" and line_number = 0 present in most of overloads of That are a real PITA because they are in majority of cases not needed, but prevent adding a default message supplier on third position. That's why new, symmetrical set of overloads has to be added. Additionally, presence of specializations for const char* (and potentially bool) makes amount of overloads even larger.

Known problems:

Overload for That(bool, messagesupplier) is not supplied, because it happens to clash with existing two-argument overloads of That(expected, expressionbuilder) and That(expected, expression). Additionally, if anyone happened to call Assert::That(boolvalue, AssertionMessage(someMessage)), they will encounter not very clear compilation error. I believe it is possible to fix it and make it work with some template mumbo-jumbo, I just do not know how yet. If you think it's necessary, I can research on this, but if someone wants to perform assertions on booleans with a custom message, they can always use Assert::That(boolvalue, IsTrue(), msgsupplier) or Assert::That(boolvalue, Is().True(), msgsupplier) (or respective equivalents IsFalse()/Is().False()).

Example usage
// #include "preloaded.h" if preloaded

// [solution]


#include <igloo/igloo_alt.h>
using namespace igloo;

// [tests]
#include <cmath>
#include <utility>
#include <cstdlib>
#include <ctime>

Describe(Overloads_And_Compatibility)
{

    It(CompatTest_With_Fluent_Expression_Without_Message)
    {
        Assert::That(3.5, Is().EqualToWithDelta(3.6, 1e-9));
    }

    It(Test_With_Fluent_Expression_And_Message)
    {
        Assert::That(3.5, Is().EqualToWithDelta(3.6, 1e-9), AssertionMessage("Invalid result for input: 3.5"));
    }

    It(CompatTest_With_StringLiteral_Fluent_Expression_Without_Message)
    {
        Assert::That("a", Is().StartingWith("b"));
    }

    It(Test_With_StringLiteral_Fluent_Expression_And_Message)
    {
        Assert::That("a", Is().StartingWith("b"), AssertionMessage("Invalid result for input: a"));
    }

    It(CompatTest_With_Expression_Without_Message)
    {
        Assert::That(3.5, EqualsWithDelta(3.6, 1e-9));
    }

    It(Test_With_Expression_And_Message)
    {
        Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), AssertionMessage("Invalid result for input: 3.5"));
    }

    It(CompatTest_With_StringLiteral_Expression_Without_Message)
    {
        Assert::That("a", StartsWith("b"));
    }

    It(Test_With_StringLiteral_Expression_And_Message)
    {
        Assert::That("a", StartsWith("b"), AssertionMessage("Invalid result for input: a"));
    }

    It(CompatTest_With_Boolean_Without_Message)
    {
        Assert::That(false);
    }

    //does not compile
    //It(Test_With_Boolean_And_Message)
    //{
    //    Assert::That(false, AssertionMessage("Wrong answer!"));
    //}
};

Describe(MessageSuppliers) {

    Describe(Test_EmptyMessageSupplier) {
        It(Test_With_Empty_Message)
        {
            Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), EmptyMessageSupplier());
        }
    };

    Describe(Test_StaticMessageSupplier) {
        It(Test_With_Static_Message)
        {
            Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), AssertionMessage("Invalid result for input: 3.5"));
        }
    };

    Describe(Test_StreamMessageBuilder) {
        It(Test_With_Message_Builder_And_Message_Prefix)
        {
            Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), MessageBuilder("Invalid result for input: ") << 3.5);
        }

        It(Test_With_Message_Builder_No_Message_Prefix)
        {
            Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), MessageBuilder() << 3.5 << " returned incorrect answer:");
        }
    };

    Describe(Test_LazyEvaluationSupplier) {

        It(Test_LazySupplier_Lambda) {

            for (int input = 0; input < 100; ++input) {
                
                //Potentially expensive way to build an assertion message.
                //Postpone it until actually needed, do not calculate the message
                //if assertion passes.
                auto makemsg = [input]() -> std::string {
                    return std::string("Invalid answer for ") + std::string(input, '#');
                };
                Assert::That(input, IsLessThan(50), makemsg);
            }
        }
    };
};

Describe(Booleans) {

    It(With_ExpressionBuilder_No_Message) {
        Assert::That(true, Is().False());
    }

    It(With_ExpressionBuilder_And_Message) {
        Assert::That(true, Is().False(), AssertionMessage("Wrong answer!"));
    }

    It(With_Expression_No_Message) {
        Assert::That(true, IsFalse());
    }

    It(With_Expression_And_Message) {
        Assert::That(true, IsFalse(), AssertionMessage("Wrong answer!"));
    }

    It(Literal) {
        Assert::That(false);
    }
};

// [main]
#include <igloo/CodewarsTestListener.h>
int main(int, const char* []) {
	NullTestResultsOutput output;
	TestRunner runner(output);
	CodewarsTestListener listener;
	runner.AddListener(&listener);
	runner.Run();
}
Example output
<DESCRIBE::>Overloads_And_Compatibility

<IT::>CompatTest_With_Fluent_Expression_Without_Message

<FAILED::>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_Fluent_Expression_And_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_StringLiteral_Fluent_Expression_Without_Message

<FAILED::>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_StringLiteral_Fluent_Expression_And_Message

<FAILED::>Invalid result for input: a<:LF:>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_Expression_Without_Message

<FAILED::>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_Expression_And_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_StringLiteral_Expression_Without_Message

<FAILED::>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_StringLiteral_Expression_And_Message

<FAILED::>Invalid result for input: a<:LF:>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_Boolean_Without_Message

<FAILED::>Expected: true<:LF:>Actual: false

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>MessageSuppliers

<COMPLETEDIN::>

<DESCRIBE::>Test_EmptyMessageSupplier

<IT::>Test_With_Empty_Message

<FAILED::>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Test_StaticMessageSupplier

<IT::>Test_With_Static_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Test_StreamMessageBuilder

<IT::>Test_With_Message_Builder_And_Message_Prefix

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_Message_Builder_No_Message_Prefix

<FAILED::>3.5 returned incorrect answer:<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Test_LazyEvaluationSupplier

<IT::>Test_LazySupplier_Lambda

<FAILED::>Invalid answer for ##################################################<:LF:>Expected: less than 50<:LF:>Actual: 50<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Booleans

<IT::>With_ExpressionBuilder_No_Message

<FAILED::>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_ExpressionBuilder_And_Message

<FAILED::>Wrong answer!<:LF:>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_Expression_No_Message

<FAILED::>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_Expression_And_Message

<FAILED::>Wrong answer!<:LF:>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>Literal

<FAILED::>Expected: true<:LF:>Actual: false

<COMPLETEDIN::>

<COMPLETEDIN::>

@hobovsky hobovsky marked this pull request as ready for review April 22, 2021 11:10
@kazk
Copy link
Member

kazk commented Apr 23, 2021

Initial thoughts.

I don't think we should or need to provide much. Adding overloads that accept a callable returning a string to specify a custom message, and providing a convenient way to specify a string should be enough.

I'd remove MessageBuilder because I don't think it's worth including.

Assert::That(
  3.5,
  EqualsWithDelta(3.6, 1e-9),
  MessageBuilder("Invalid result for input: ") << 3.5
);
// vs
Assert::That(
  3.5,
  EqualsWithDelta(3.6, 1e-9),
  AssertionMessage(fmt::format("Invalid result for input: {}", 3.5))
);
Assert::That(
  3.5,
  EqualsWithDelta(3.6, 1e-9),
  MessageBuilder() << 3.5 << " returned incorrect answer"
);
// vs
Assert::That(
  3.5,
  EqualsWithDelta(3.6, 1e-9),
  AssertionMessage(fmt::format("{} returned incorrect answer", 3.5))
);

I'd also use []() { return ""; } instead of EmptyMessageSupplier() internally. Users don't need it, right?

Instead of having MessageStringSupplier, AssertionMessage can be

auto AssertionMessage = [](const std::string& msg) { return [&]() { return msg; }; };

Might want a better name for AssertionMessage. Assert that, x equals with y, assertion message is awkward to read. MessageOnFailure?

@hobovsky
Copy link
Author

hobovsky commented Apr 23, 2021

I'd remove MessageBuilder because I don't think it's worth including.

I created the stream-based message supplier thinking about people like me, who have no idea about {fmt}, do not know how to use it in a kata (what includes do I need to add?), and/or do not want to add additional dependencies and prefer to stick to std. {fmt} is an additional dependency, and everyone who'd like to train on a kata locally would need to have it installed. We'd also have to document it better with CW docs related to C++.
But we do not need to provide it, it can be put into docs as a snippet for users to pick from there.

I'd also use []() { return ""; } instead of EmptyMessageSupplier() internally. Users don't need it, right?

I was considering making it a functor private to the library, but finally I decided to make it public. I thought that some users might want to have it, to use new overloads but keep the old behavior, or to switch implementations. Not that I have any specific use case for this. After all, you have things like Identity too, right? ;) But sure, it's not necessary, and users can create one if they want to.

Additionally, I used MessageStreamSupplier and EmptyMessageSupplier as a proof that my idea of having a template parameter with concrete implementations possible to provide by the user would work. And since I've already created them, I just left them there. Also mind that any thing which we leave on authors to provide, they might to do incorrectly and/or create additional maintenance work.

Instead of having MessageStringSupplier, AssertionMessage can be

Sure. I went with a functor for a couple of reasons:

  • I vaguely remember functions not being completely equivalent to functors, and one needed to use adapters like std::ptr_fun and similar. But since now all function adapters seem to be deprecated, I think C++ managed to rectify related problems.
  • I am much older than C++ lambdas, I hate syntax of C++ lambdas.
  • I remember VC++ having problems with user-friendly presentation of lambda types with intellisense etc.

But it's time to grow up, and lambdas got better support from VC++ nowadays, so why not ;)

Might want a better name for AssertionMessage. Assert that, x equals with y, assertion message is awkward to read. MessageOnFailure?

I am somewhat concerned by ambiguity of MessageOnFailure: will tests still provide the Expected: x\nActual: y part, or should I provide the complete message now? IIRC different testing frameworks on CW work differently in this matter: some just add a custom part to the failure message, and some replace the default message completely. But I have no good idea for another name either. I was thinking WithMessage.

- EmptyMessageSupplier and MessageStringSupplier changed from functors to lambdas
- Removed MessageStreamSupplier
- Renamed AssertionMessage to WithMessage
@hobovsky
Copy link
Author

Per suggestions:

  • EmptyMessageSupplier and MessageStringSupplier changed from functors to lambdas
  • Removed MessageStreamSupplier, can be documented as a snippet for users who'd want to use it.
  • Renamed AssertionMessage to WithMessage as a proposal
Example usage
// #include "preloaded.h" if preloaded

// [solution]


#include <igloo/igloo_alt.h>
using namespace igloo;

// [tests]
#include <cmath>
#include <utility>
#include <cstdlib>
#include <ctime>

Describe(Overloads_And_Compatibility)
{

    It(CompatTest_With_Fluent_Expression_Without_Message)
    {
        Assert::That(3.5, Is().EqualToWithDelta(3.6, 1e-9));
    }

    It(Test_With_Fluent_Expression_And_Message)
    {
        Assert::That(3.5, Is().EqualToWithDelta(3.6, 1e-9), WithMessage("Invalid result for input: 3.5"));
    }

    It(CompatTest_With_StringLiteral_Fluent_Expression_Without_Message)
    {
        Assert::That("a", Is().StartingWith("b"));
    }

    It(Test_With_StringLiteral_Fluent_Expression_And_Message)
    {
        Assert::That("a", Is().StartingWith("b"), WithMessage("Invalid result for input: a"));
    }

    It(CompatTest_With_Expression_Without_Message)
    {
        Assert::That(3.5, EqualsWithDelta(3.6, 1e-9));
    }

    It(Test_With_Expression_And_Message)
    {
        Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), WithMessage("Invalid result for input: 3.5"));
    }

    It(CompatTest_With_StringLiteral_Expression_Without_Message)
    {
        Assert::That("a", StartsWith("b"));
    }

    It(Test_With_StringLiteral_Expression_And_Message)
    {
        Assert::That("a", StartsWith("b"), WithMessage("Invalid result for input: a"));
    }

    It(CompatTest_With_Boolean_Without_Message)
    {
        Assert::That(false);
    }

    //does not compile
    //It(Test_With_Boolean_And_Message)
    //{
    //    Assert::That(false, AssertionMessage("Wrong answer!"));
    //}
};

Describe(MessageSuppliers) {

    Describe(Test_StaticMessageSupplier) {
        It(Test_With_Static_Message)
        {
            Assert::That(3.5, EqualsWithDelta(3.6, 1e-9), WithMessage("Invalid result for input: 3.5"));
        }
    };

    Describe(Test_LazyEvaluationSupplier) {

        It(Test_LazySupplier_Lambda) {

            for (int input = 0; input < 100; ++input) {
                
                //Potentially expensive way to build an assertion message.
                //Postpone it until actually needed, do not calculate the message
                //if assertion passes.
                auto makemsg = [input]() -> std::string {
                    return std::string("Invalid answer for ") + std::string(input, '#');
                };
                Assert::That(input, IsLessThan(50), makemsg);
            }
        }
    };
};

Describe(Booleans) {

    It(With_ExpressionBuilder_No_Message) {
        Assert::That(true, Is().False());
    }

    It(With_ExpressionBuilder_And_Message) {
        Assert::That(true, Is().False(), WithMessage("Wrong answer!"));
    }

    It(With_Expression_No_Message) {
        Assert::That(true, IsFalse());
    }

    It(With_Expression_And_Message) {
        Assert::That(true, IsFalse(), WithMessage("Wrong answer!"));
    }

    It(Literal) {
        Assert::That(false);
    }
};

// [main]
#include <igloo/CodewarsTestListener.h>
int main(int, const char* []) {
	NullTestResultsOutput output;
	TestRunner runner(output);
	CodewarsTestListener listener;
	runner.AddListener(&listener);
	runner.Run();
}
Example output

<DESCRIBE::>Overloads_And_Compatibility

<IT::>CompatTest_With_Fluent_Expression_Without_Message

<FAILED::>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_Fluent_Expression_And_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_StringLiteral_Fluent_Expression_Without_Message

<FAILED::>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_StringLiteral_Fluent_Expression_And_Message

<FAILED::>Invalid result for input: a<:LF:>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_Expression_Without_Message

<FAILED::>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_Expression_And_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_StringLiteral_Expression_Without_Message

<FAILED::>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>Test_With_StringLiteral_Expression_And_Message

<FAILED::>Invalid result for input: a<:LF:>Expected: starts with "b"<:LF:>Actual: "a"<:LF:>

<COMPLETEDIN::>

<IT::>CompatTest_With_Boolean_Without_Message

<FAILED::>Expected: true<:LF:>Actual: false

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>MessageSuppliers

<COMPLETEDIN::>

<DESCRIBE::>Test_StaticMessageSupplier

<IT::>Test_With_Static_Message

<FAILED::>Invalid result for input: 3.5<:LF:>Expected: equal to 3.6 (+/- 1e-09)<:LF:>Actual: 3.5<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Test_LazyEvaluationSupplier

<IT::>Test_LazySupplier_Lambda

<FAILED::>Invalid answer for ##################################################<:LF:>Expected: less than 50<:LF:>Actual: 50<:LF:>

<COMPLETEDIN::>

<COMPLETEDIN::>

<DESCRIBE::>Booleans

<IT::>With_ExpressionBuilder_No_Message

<FAILED::>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_ExpressionBuilder_And_Message

<FAILED::>Wrong answer!<:LF:>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_Expression_No_Message

<FAILED::>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>With_Expression_And_Message

<FAILED::>Wrong answer!<:LF:>Expected: false<:LF:>Actual: true<:LF:>

<COMPLETEDIN::>

<IT::>Literal

<FAILED::>Expected: true<:LF:>Actual: false

<COMPLETEDIN::>

<COMPLETEDIN::>

@kazk
Copy link
Member

kazk commented Apr 23, 2021

do not know how to use it in a kata (what includes do I need to add?)

#include <fmt/core.h>

and/or do not want to add additional dependencies and prefer to stick to std. {fmt} is an additional dependency, and everyone who'd like to train on a kata locally would need to have it installed.

I bet C++20 (std::format) will be added before this feature becomes widely used. Also, the tests can be already using {fmt} (output to stdout) or any other extra dependencies we provide. If it's still an issue, they can remove the custom message in sample tests without a significant loss because they can see how exactly it's tested.

We'd also have to document it better with CW docs related to C++.

We need to document this either way.

Not that I have any specific use case for this. After all, you have things like Identity too, right? ;)

We shouldn't add code if there's no use case. Identity function exists because it's useful.

Also mind that any thing which we leave on authors to provide, they might to do incorrectly and/or create additional maintenance work.

Yeah, authors can misuse whatever we provide. But I don't know how MessageBuilder or EmptyMessageSupplier helps with that. I don't see why they would want to implement stream-based supplier, especially with std::format, to begin with. Even if there's a valid use case, I think it's extremely rare, and it should not be in a library. Same for EmptyMessageSupplier.

Even if we include it, that won't prevent them from writing their own, or causing maintenance work by writing awful code with them.

It should be possible to add static assertions to restrict the type of the supplier somehow. I'd rather do that if we care.

Copy link
Member

@kazk kazk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the commented out code and format to match the codebase (indentation and snake_case parameter)?

@kazk
Copy link
Member

kazk commented Apr 23, 2021

@error256 Can you review this and let us know your thoughts?

@@ -21,19 +24,31 @@

namespace snowhouse
{
auto WithMessage = [](const std::string& msg) { return [&]() { return msg; }; };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize how often we had to specify the empty message supplier.

Let's add

  namespace detail {
    auto EmptyMessage = []() { return std::string(); };
  }

and use that. That's more readable.

Copy link
Author

@hobovsky hobovsky Apr 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this won't violate the One Definition Rule? EmptyMessage will become an object, and it will be defined in every translation unit which would have assert.h included, leading to multiple definitions and linker errors. The same applies to WithMessage\ExtraMessage defined as a named symbol in line 27. I might be wrong, and my VS2019 seems to compile and link correctly when I add another translation unit which includes Igloo, but I am not 100% sure it's valid code.

Am I wrong?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made both EmptyMessage and ExtraMessage inline functions to avoid violation of ODR and problems with linking. They could probably be made inline "named lambdas", but my VS2019 seems to not like inline variables and fails to compile them. I hesitate to make them "regular, named lambdas" because I think they might violate ODR (I am not sure though, my C++ got really rusty :( ), but if you are sure they won't, I can do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know C++ much. It might be a violation. https://stackoverflow.com/a/62150779
I thought assert.h having a header guard helps, but not sure.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guards won't help because then it won't be declared in other translation units. I think inline variables should be good. What do you mean by "regular, named lambdas"? Usual functor classes can use inline member functions, so I don't think they'll have ODR issues.
Are lvalue instances of WithMessage/ExtraMessage supposed to exist? I haven't looked at the code thoroughly, but with using strings by reference it looks like there will be some issues if someone creates an lvalue WithMessage from an rvalue string.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use them, but my VS2019 complained. Either it uses C++14 compliant compiler, or this C++17 feature is not supported, or I did something wrong.

This is the first time I've seen inline variables, but it looks like this is exactly the case they exist for. What about static?

but since the thing is named, it's not a lambda anymore, right?

I guess a lambda is always a lambda. Its class name is always unique and unknown.

such variables defined in a header can (can they really?) violate ODR.

I think they can. If you repeat it in another translation unit, it's the second definition, otherwise there's no declaration. Normally, a definition should be in a separate translation unit, but it's a header-only library.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I think that EmptyMessage and ExtraMessage are perfectly fine as they are: inline, free functions. I'd say there's no need, and no benefit, in making them lambda-initialized variables, be it marked as inline or static or whatever, just for sake of turning functions into lambda expressions. Is there anything in particular better in

    inline auto EmptyMessage = []() { return std::string(); };

vs

    inline std::string EmptyMessage()
    { 
      return std::string();
    }

I personally think there's not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I don't care if these are lambdas or functions, and I don't think there's a significant difference we'd care.

Originally suggested changing because I thought structs were unnecessary, especially since those were brought into scope. The lambda version compiled and worked as expected, so I thought that was good (spoiled by Rust).

Tested the latest version against all published C++ kata and had the same result (no error or new warning).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested the latest version against all published C++ kata and had the same result (no error or new warning).

Great! When (and if) we get it deployed, I could test it on some kata from C++ list when fixing warnings.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create some docs for cpp too similar to what we have for C or JS: some kind of reference, and authoring tutorial.

- fixed indentation
- argument names changed to snake_case
- removed commented code
- renamed `WithMessage` to `ExtraMessage`
- reintroduced `EmptyMessage`
@hobovsky hobovsky requested a review from kazk April 24, 2021 01:21
Copy link
Member

@kazk kazk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, but I want someone who knows C++ more to review. I'll try testing against existing kata to make sure this doesn't break them.

@kazk kazk changed the title Custom assertion messages by message supplier Add overloads to show additional custom message on failure Apr 26, 2021
@kazk kazk merged commit cc6e9ff into codewars:headers-only-codewars Apr 26, 2021
kazk added a commit to codewars/igloo that referenced this pull request Apr 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants