const_format_crates icon indicating copy to clipboard operation
const_format_crates copied to clipboard

Generic support is possible?

Open smmoosavi opened this issue 2 years ago • 10 comments

Feature request

I tried to use formatcp with generics, which caused an error. Is it possible to formatcp support generics?

    use const_format::formatcp;

    pub trait Named {
        const NAME: &'static str;
    }

    struct Foo;

    impl Named for Foo {
        const NAME: &'static str = "Foo";
    }

    struct Box<T>(T);

    impl<T: Named> Named for Box<T> {
        const NAME: &'static str = formatcp!("{}Box", <T as Named>::NAME);
    }
    
    #[test]
    fn test() {
        assert_eq!(Foo::NAME, "Foo");
        assert_eq!(Box::<Foo>::NAME, "FooBox");
    }

errors

error[E0401]: can't use generic parameters from outer function
   |
   |     impl<T: Named> Named for Box<T> {
   |          - type parameter from outer function
   |         const NAME: &'static str = formatcp!("{}Box", <T as Named>::NAME);
   |                                                        ^ use of generic parameter from outer function

error[E0080]: evaluation of constant value failed
   |
   |         const NAME: &'static str = formatcp!("{}Box", <T as Named>::NAME);
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ referenced constant has errors
   |
   = note: this error originates in the macro `$crate::pmr::__formatcp_impl` which comes from the expansion of the macro `formatcp` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0080]: evaluation of constant value failed
   |
   |         const NAME: &'static str = formatcp!("{}Box", <T as Named>::NAME);
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ referenced constant has errors
   |
   = note: this error originates in the macro `__cf_osRcTFl4A::__concatcp_inner` which comes from the expansion of the macro `formatcp` (in Nightly builds, run with -Z macro-backtrace for more info)

smmoosavi avatar Feb 08 '23 19:02 smmoosavi

This would be really useful. If this worked, it could reduce heap allocations in my crate to almost zero, as opposed to multiple String allocations on every query.

Einliterflasche avatar Sep 14 '23 19:09 Einliterflasche

Any updates? It is really useful

YjyJeff avatar Dec 18 '23 02:12 YjyJeff

I think this is a limitation of the rust language, not of this library. There is some initiative which is planning to support custom types as const parameters but I don't remember the name.

Einliterflasche avatar Dec 18 '23 08:12 Einliterflasche

Is there an alternative solution, I'm having the same problem😢

acking-you avatar Dec 27 '23 14:12 acking-you

It could be done like this:

pub trait Named {
    const NAME: &'static str;
}

struct Foo;

impl Named for Foo {
    const NAME: &'static str = "Foo";
}

struct Box<T>(T);

impl<T: Named> Named for Box<T> {
    const NAME: &'static str = buf_and_len_to_str(&concat_buf(<T as Named>::NAME, "Box"));
}

const fn buf_and_len_to_str(buf_len: &'static ([u8; 60], usize)) -> &'static str {
    let buf = &buf_len.0;
    let len = buf_len.1;
    assert!(len < buf.len(), "buf is too long");
    // I didn't find a way to slice an array in const fn
    let buf = unsafe { core::slice::from_raw_parts(buf.as_ptr(), len) };
    match core::str::from_utf8(buf) {
        Ok(s) => s,
        Err(_) => panic!(),
    }
}

const fn concat_buf(left: &'static str, right: &'static str) -> ([u8; 60], usize) {
    let mut buf = [0u8; 60];
    let mut i = 0;
    while i < left.len() {
        buf[i] = left.as_bytes()[i];
        i += 1;
    }
    while i - left.len() < right.len() {
        buf[i] = right.as_bytes()[i - left.len()];
        i += 1;
    }

    (buf, i)
}

#[test]
fn test() {
    assert_eq!(Foo::NAME, "Foo");
    assert_eq!(Box::<Foo>::NAME, "FooBox");
}

One problem (that can be mitigated) is that the whole buf would be in the final binary:

image

7th are got from changing let mut buf = [0u8; 60]; to let mut buf = [b'7'; 60];

Ddystopia avatar Dec 31 '23 15:12 Ddystopia

Wow, thats really ~~hacky~~ awesome! When you said it can be mitigated, you didn't by any chance mean the whole buffer ending up in the binary? Probably not... still many thanks :D

Einliterflasche avatar Dec 31 '23 16:12 Einliterflasche

you didn't by any chance mean the whole buffer ending up in the binary? Probably not... still many thanks :D

You can compute the length of the buffer and pass it by generics, so no space will be wasted. But even without that, overhead is very low, std stuff, panic debug prints, debuginfo etc are using a lot more space

Ddystopia avatar Dec 31 '23 16:12 Ddystopia

Sorry for not responding this entire time.

@Ddystopia You're right that you can do it with a fixed size buffer, but the issue with this approach is that it can do any of these:

  • leave the concatenated string truncated
  • panic because the returned buffer isn't large enough to fit the concatenated strings
  • (conditional on truncation) panic because the returned buffer doesn't end in valid utf8

(I believe that the const_generic_exprs unstable feature is necessary to do this properly, so that the resulting string is exactly as long as needed, but I haven't used this feature at all so I don't know its limitations)

Also, you can subslice a slice in const (since Rust 1.71.0) with split_at.

rodrimati1992 avatar Dec 31 '23 17:12 rodrimati1992

Sorry for not responding this entire time.

@Ddystopia You're right that you can do it with a fixed size buffer, but the issue with this approach is that it can do any of these:

* leave the concatenated string truncated

* panic because the returned buffer isn't large enough to fit the concatenated strings

* (conditional on truncation) panic because the returned buffer doesn't end in valid utf8

(I believe that the const_generic_exprs unstable feature is necessary to do this properly, so that the resulting string is exactly as long as needed, but I haven't used this feature at all so I don't know its limitations)

Also, you can subslice a slice in const (since Rust 1.71.0) with split_at.

I want to make it clear that I left this here because someone wanted a workaround. Of course, it's too limited to be in this library. It's just that someone said that this feature would help them get rid of allocations, and for the sake of that, this kind of thing is as good as it gets. Thank you for your response

Also, that code will always panic when buffer is not large enough and will not leave concatenated string truncated. There will also be no utf-8 issues. The reason is that copy loops are not dependent over the buffer length.

Ddystopia avatar Jan 01 '24 08:01 Ddystopia

About the incomplete generic_const_exprs feature. It can be done, but it is even more unergonomic than the previous workaround.

#![feature(generic_const_exprs)]

pub trait Named {
    const NAME: &'static str;
}

struct Foo;

impl Named for Foo {
    const NAME: &'static str = "Foo";
}

struct Box<T>(T);

impl<T: Named> Named for Box<T>
where
    [(); <T as Named>::NAME.len() + "Box".len()]:,
{
    const NAME: &'static str = {
        buf_and_len_to_str::<{ <T as Named>::NAME.len() + "Box".len() }>(&concat_buf::<
            { <T as Named>::NAME.len() + "Box".len() },
        >(<T as Named>::NAME, "Box"))
    };
}

const fn buf_and_len_to_str<const LEN: usize>(buf_len: &([u8; LEN], usize)) -> &str {
    let buf = &buf_len.0;
    let len = buf_len.1;
    let buf = buf.split_at(len).0;
    match core::str::from_utf8(buf) {
        Ok(s) => s,
        Err(_) => panic!(),
    }
}

const fn concat_buf<const LEN: usize>(left: &str, right: &str) -> ([u8; LEN], usize) {
    let mut buf = [b'7'; LEN];
    let mut i = 0;
    while i < left.len() {
        buf[i] = left.as_bytes()[i];
        i += 1;
    }
    while i - left.len() < right.len() {
        buf[i] = right.as_bytes()[i - left.len()];
        i += 1;
    }

    (buf, i)
}

or

pub trait Len {
    const LEN: usize;
}

impl Len for Foo {
    const LEN: usize = <Self as Named>::NAME.len();
}

impl<T: Len> Len for Box<T> {
    const LEN: usize = <T as Len>::LEN + "Box".len();
}

impl<T: Named + Len> Named for Box<T>
where
    [(); <Self as Len>::LEN]:,
{
    const NAME: &'static str = {
        buf_and_len_to_str::<{ Self::LEN }>(&concat_buf::<{ Self::LEN }>(<T as Named>::NAME, "Box"))
    };
}

Ddystopia avatar Jan 01 '24 08:01 Ddystopia