Skip to content

Add check if type is complete to before checking sizeof type in prefer_pass_by_value check #276

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

Conversation

filipsajdak
Copy link
Contributor

In the current implementation of prefer_pass_by_value, we check the size of the type. It fails in case that type is incomplete. This change adds a check if there is a possibility to check the size of the type.

This might be a temporary solution but it can help avoid failed builds.

@JohelEGP
Copy link
Contributor

To help avoid failed builds, I think this should be a requires check.
In its current form, it seems like a source for ODR violations.

@JohelEGP
Copy link
Contributor

JohelEGP commented Mar 12, 2023

To help avoid failed builds, I think this should be a requires check.

Rather than avoid failed builds, it actually fails with a more meaningful error message.

The current formulation still fails because the use of sizeof, even if not evaluated due to the short-circuit, doesn't permit an incomplete type. See https://compiler-explorer.com/z/j9zs675YY.

@SebastianTroy
Copy link

SebastianTroy commented Mar 12, 2023 via email

@JohelEGP
Copy link
Contributor

The error message seems shorter and clearer if in directly has the requires-clause. https://compiler-explorer.com/z/K4KcjeTqj.

You can see how the meaning of in changes depending on the completeness of X at https://compiler-explorer.com/z/57oK9jEhj by (de)commenting void f(in<X>);. That probably runs afoul of https://eel.is/c++draft/temp.res.general#6 or some other rule. As you can see, the meaning of in<X> is fixed to const X& if it's used while X is incomplete.

@filipsajdak
Copy link
Contributor Author

@JohelEGP Thank you for spotting that! What about this: https://compiler-explorer.com/z/9b7bcEf8c

template<typename T, typename = void>
constexpr bool prefer_pass_by_value = false;

template<typename T>
constexpr bool prefer_pass_by_value<T, std::void_t<decltype(sizeof(T))>> =
    sizeof(T) < 2*sizeof(void*)
    && std::is_trivially_copy_constructible_v<T>;

Can you tell me why it might be an ODR violation?

In current implementation of `prefer_pass_by_value` we check sizeof type.
It fails in case that type is incomplete. This change adds a check if
there is a possibility to check the sizeof type.
@filipsajdak filipsajdak force-pushed the fsajdak-in-arg-pass-check-type-completeness branch from d894a91 to 6cd3450 Compare March 13, 2023 00:36
@filipsajdak
Copy link
Contributor Author

@JohelEGP Just to clarify, the order of #270 (comment) should look like following:

// declaration of classes (UDT)
struct X {
    int i;
    double d;

    void fun();
    double gun();
};

// declarations of free functions
void f(in<X>);

// implementations of class functions
void X::fun() {
    // code
}

double X::gun() {
    return i * d;
}

// implementations of free functions
void f(in<X>) { }

@filipsajdak
Copy link
Contributor Author

My issue is that I have a project divided to several .h2 files that compiles to .h and .hpp files. When I include the .h2 file in .cpp2 file it is later replaced with .h file in declaration section and .hpp in definitions sections.

My .h2 file looks like the following:

#include <map>

namespace execspec {
    using config_t = std::map<std::string, std::string>;
}

execspec: namespace = {

    process_stage : type = {
        public name: std::string;
        public cmd:  std::string;
    }

    spec: type = {
        name:        std::string;
        description: std::string = "";
        config:      execspec::config_t = ();
        stages:      std::vector<execspec::process_stage> = ();

        operator=: (out this, n: std::string) = {
            name = n;
        }

        get_name:        (this) -> forward _ = name;
        get_description: (this) -> forward _ = description;
        get_config:      (this) -> forward _ = config;
        get_stages:      (this) -> forward _ = stages;

// skipping some code
    }
}

debug: (inout o : std::ostream, spec : execspec::spec) -> forward std::ostream = {
    o << "# (spec.get_name())$\n";

// skipping some code

    return o;
}

it generates .h file (skipping boilerplate):

#include <map>

namespace execspec {
    using config_t = std::map<std::string, std::string>;
}

#include "cpp2util.h"

#line 7 "spec.h2"
namespace execspec   {
    class process_stage;
    class spec;
};
#line 49 "spec.h2"
[[nodiscard]] auto debug(std::ostream& o, cpp2::in<execspec::spec> spec) -> std::ostream&;

.hpp file has all definitions that normally would be generated in .cpp file when compiling above code as .cpp2 file.

I have an issue when I include spec.h2 file in other .cpp2 file - in my case I include it in spec_test.cpp2 file that looks following:

#include "boost/ut.hpp"
using namespace boost::ut;

#include <sstream>
using namespace std::literals;

#include "execspec/spec.h2"

main: () -> int = {
    execspec__spec : suite = :() = {
        "create a spec"_test = :() = {
            t := execspec::spec("spec name"s);

            o : std::ostringstream = ();
            o.debug(t);
            
            expect(eq(o.str(), "# spec name\n\n"sv));
        };
    };
}

it generates:

#include "boost/ut.hpp"
using namespace boost::ut;

#include <sstream>
using namespace std::literals;

#include "execspec/spec.h"

#include "cpp2util.h"


#line 9 "spec_tests.cpp2"
[[nodiscard]] auto main() -> int;

//=== Cpp2 definitions ==========================================================

#include "execspec/spec.hpp"
#line 8 "spec_tests.cpp2"

[[nodiscard]] auto main() -> int{
    suite execspec__spec {[]() -> void{
        "create a spec"_test = []() -> void{
            auto t {execspec::spec("spec name"s)}; 

            std::ostringstream o {}; 
            CPP2_UFCS(debug, o, t);

            expect(eq(CPP2_UFCS_0(str, o), "# spec name\n\n"sv));
        };
    };
}

And that fail with an error:

[build] In file included from spec_tests.cpp2:7:
[build] In file included from spec.h2:7:
[build] cppfront/include/cpp2util.h:496:5: error: invalid application of 'sizeof' to an incomplete type 'execspec::spec'
[build]     sizeof(T) < 2*sizeof(void*) && std::is_trivially_copy_constructible_v<T>;
[build]     ^~~~~~~~~
[build] cppfront/include/cpp2util.h:501:9: note: in instantiation of variable template specialization 'cpp2::prefer_pass_by_value<execspec::spec>' requested here
[build]         prefer_pass_by_value<T>,
[build]         ^
[build] spec.h2:49:49: note: in instantiation of template type alias 'in' requested here
[build] [[nodiscard]] auto debug(std::ostream& o, cpp2::in<execspec::spec> spec) -> std::ostream&;
[build]                                                 ^
[build] spec.h2:9:11: note: forward declaration of 'execspec::spec'
[build]     class spec;
[build]           ^
[build] In file included from spec_tests.cpp2:7:
[build] In file included from spec.h2:7:
[build] cppfront/include/cpp2util.h:501:9: error: non-type template argument is not a constant expression
[build]         prefer_pass_by_value<T>,
[build]         ^
[build] spec.h2:49:49: note: in instantiation of template type alias 'in' requested here
[build] [[nodiscard]] auto debug(std::ostream& o, cpp2::in<execspec::spec> spec) -> std::ostream&;
[build]                                                 ^
[build] 2 errors generated.

With my current patch it compiles fine.

@filipsajdak
Copy link
Contributor Author

I get that practically this needs to build so needs must, but as cpp2 is intended to be order independant, would a type being declared but not defined be an issue for end users to worry about still?

@SebastianTroy eventually, it will not be a problem - it is just a matter of generating the output files in a specific order - example here: #276 (comment). The current issue is caused by the introduction of classes (which is an immature implementation).

The issue is more problematic when you split your project into multiple .cpp2 and .h2 files. Most of our current examples are simple .cpp2 files.

@filipsajdak
Copy link
Contributor Author

Taking another look at this... I think that an ODR violation might be an issue here. Probably I will need to temporarily change the meaning of in<T> to always T const& unless someone has a better idea.

@JohelEGP
Copy link
Contributor

Can you tell me why it might be an ODR violation?

I wish I could give a methodical answer with proof and references. But I used might for a reason. The gist is usually that T<U> shouldn't change meaning. At https://compiler-explorer.com/z/3aGq5KzcG is a valid, non ODR violating (AFAIK) formulation that demonstrates the change in meaning. (See https://eel.is/c++draft/temp.arg.general#9, https://compiler-explorer.com/z/98o99r6v9 for https://eel.is/c++draft/temp.inst#7.sentence-2 [likely a GCC bug]).

@JohelEGP
Copy link
Contributor

JohelEGP commented Mar 13, 2023

Taking another look at this... I think that an ODR violation might be an issue here. Probably I will need to temporarily change the meaning of in<T> to always T const& unless someone has a better idea.

https://compiler-explorer.com/z/Pv69dWqvE. Making an exposition-only type that's complete earlier. This can only really save Cpp2 types. Incomplete Cpp1 types could be covered by a general statement like https://eel.is/c++draft/res.on.functions#2.5.

@filipsajdak filipsajdak deleted the fsajdak-in-arg-pass-check-type-completeness branch March 22, 2023 08:36
@JohelEGP
Copy link
Contributor

Resolved by a89af8f:

[...]
Changed in to pass fundamental types by value. Only those are known to be always complete. Partly addresses #270.
[...]

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